diff --git a/changes/unreleased/atomic-body-statement-order.fixed.md b/changes/unreleased/atomic-body-statement-order.fixed.md new file mode 100644 index 0000000000..a3e00b9071 --- /dev/null +++ b/changes/unreleased/atomic-body-statement-order.fixed.md @@ -0,0 +1 @@ +- **Exploration and the model checker reach every order of a calc or constraint body's unordered statements.** A calc body whose `assign y := y * 10;` and `assign y := y + 2;` no `then` relates now explores and checks to both `12` and `30` instead of claiming `12` over every schedule, and a constraint whose verdict depends on the order of its body's statements is both satisfied and violated, and violated under `-engine check`. The body is still performed whole inside the step that evaluates it, so its statements are reordered among themselves and never interleaved with another performance; its result expression or condition is evaluated after them. Each invocation whose result depends on the order is one `statement order` choice point between the distinct results, so a recursive calc adds one choice rather than one per level, and a body whose statements commute adds none. A guard whose verdict depends on the order is a `statement order` choice point between the distinct results the orders produce, an evaluation error among them; inside a preview it is reported as not covered, as is a guard whose constraint body can write outside the constraint. `then` between a constraint body's own statements now orders them rather than being refused. Calcs compiled at run time fall back to the interpreter when their statements can reorder under these schedules. `declared` and the default schedule keep declaration order, so default results do not move. A stated succession a calc body cannot perform is refused naming the construct (`` `first` statement``) rather than an internal type. diff --git a/changes/unreleased/case-step-order.fixed.md b/changes/unreleased/case-step-order.fixed.md new file mode 100644 index 0000000000..2054f681d4 --- /dev/null +++ b/changes/unreleased/case-step-order.fixed.md @@ -0,0 +1,6 @@ +- The steps of an analysis or verification case that states no succession are now unordered, as an + action definition's subactions are: `explore`, `-engine check`, replay and seeded runs reach every + 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. diff --git a/changes/unreleased/constraint-body-steps.added.md b/changes/unreleased/constraint-body-steps.added.md new file mode 100644 index 0000000000..35832b6d8c --- /dev/null +++ b/changes/unreleased/constraint-body-steps.added.md @@ -0,0 +1 @@ +- Statements in a constraint or requirement body (`assign`, `if`, `while`/`loop`/`for`, and body-local declarations interleaved with them) now run as steps of the check's own performance before its conditions are evaluated, in declaration order: locals live in a fresh frame per check, the constraint's own parameters are copied into it, and each condition of the body is judged in the state the steps left. Assignments reaching outside the performance — a name it holds no feature for, a chained or qualified target — and effects it cannot perform (`send`, `perform`, `terminate`) are refused with the typed `ErrConstraintExternalAssignment`/`ErrConstraintEffect`, as are stated successions (`ErrStatementNotExecutable`), and a body stating steps but no result expression reports `ErrNoConditions`. The solver refuses to translate the steps of such a body rather than solving its conditions as if they ran. diff --git a/changes/unreleased/do-flow-node-yields.fixed.md b/changes/unreleased/do-flow-node-yields.fixed.md new file mode 100644 index 0000000000..ce2e88f77a --- /dev/null +++ b/changes/unreleased/do-flow-node-yields.fixed.md @@ -0,0 +1 @@ +- **A loop or `if` node of a state's do body stating a flow yields as a statement does.** Adding `then` to a do body made it a stated flow whose `for`, `while` and `if` nodes ran whole in one do round, so two regions' bodies no longer interleaved inside them. Such a node now gives the machine up after each iteration and each branch statement, as the statement-list body does, under every schedule; explore, the model checker, replay and seeds reach those interleavings. diff --git a/changes/unreleased/explore-body-interleavings.fixed.md b/changes/unreleased/explore-body-interleavings.fixed.md new file mode 100644 index 0000000000..9ce9c7240b --- /dev/null +++ b/changes/unreleased/explore-body-interleavings.fixed.md @@ -0,0 +1 @@ +- **Exploration and the model checker reach interleavings inside concurrent action bodies.** A leaf action body's initial values are read when its performance starts and each `assign` writes when it ends, so another concurrent performance may run between them: two performances of `action a[2] { attribute t : Integer := c; assign c := t + 1; }` now explore to `c = 1` and `c = 2` instead of claiming `c = 2` over every schedule, and two fork branches with that body do the same. Such a body yields after its start and after each statement under `explore`, `-engine check`, replay and seeded schedules wherever another performance's moves can change its result; `reverse` and `declared` keep their results, and the SMT engine reports such a flow as not covered (`body interleaving`) rather than encoding one order. The same holds when each branch performs that body as an action of its own (`action a : Inc;` or `perform action pa : Inc;`), and between an `if`'s guard and its branch. diff --git a/changes/unreleased/explore-statement-order.fixed.md b/changes/unreleased/explore-statement-order.fixed.md new file mode 100644 index 0000000000..12c4bef0a5 --- /dev/null +++ b/changes/unreleased/explore-statement-order.fixed.md @@ -0,0 +1 @@ +- **Exploration and the model checker reach every order of a body's unordered statements.** The direct statements of one action, state behavior or calculation body that no succession orders are subactions the library leaves unordered, so `{ assign x := 1; assign y := x; }` now explores and checks to `y = 0` and `y = 1` instead of claiming `y = 1` over every schedule. `then` between statements, an `if`'s guard before its branch and a loop's iterations stay ordered; two statements that touch nothing the other does are explored once. `explore`, `-engine check`, replay and seeded schedules choose the order, recorded as a `statement order` choice point; `declared` and `reverse` run a nested body's statements first to last as before, and an action definition's own statements in the token order each policy already gave them (`reverse` last to first), so default results do not move. An `explore` run whose body has more orders than its run budget reports the search incomplete, and the SMT engine reports such a body as not covered (`statement order`) rather than encoding one order. diff --git a/changes/unreleased/extent-namespace-bindings-and-performances.fixed.md b/changes/unreleased/extent-namespace-bindings-and-performances.fixed.md new file mode 100644 index 0000000000..bee0f50501 --- /dev/null +++ b/changes/unreleased/extent-namespace-bindings-and-performances.fixed.md @@ -0,0 +1 @@ +- **`all T` now answers three namespace-level shapes it used to get wrong.** Binding connectors owned by a package (`bind a = b`, `bind c = d.w2`) join their ends into one equivalence class denoting the class's one object — several bindings on one usage and bindings written in another package included — instead of each end naming its own; the objects an object's action, state, connection, interface, allocation and flow usages hold — perform and exhibit included — are reached by an extent whose target they may hold; and a package-level collection usage denotes the objects the usages subsetting it denote first, anonymous members only to make up its lower bound, an under-count or over-count a typed `ErrMultiplicityViolation` naming it. A bound class's shared lower bound and a namespace collection's own fill past its subsetters' values are held lazily past 1000 members, as a collection's own lower bound is, so `bind a = b` over `Car[1000000000]` and a subsetter of one count and index without allocating it. diff --git a/changes/unreleased/terminate-usage-body-flow.fixed.md b/changes/unreleased/terminate-usage-body-flow.fixed.md new file mode 100644 index 0000000000..4bfd1e8240 --- /dev/null +++ b/changes/unreleased/terminate-usage-body-flow.fixed.md @@ -0,0 +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. diff --git a/cmd/sysml/check_test.go b/cmd/sysml/check_test.go index f75e0e3b49..bc1f8bdf40 100644 --- a/cmd/sysml/check_test.go +++ b/cmd/sysml/check_test.go @@ -195,6 +195,22 @@ func TestEvalAfterInstantiateThroughCLI(t *testing.T) { rejectReport(t, answered, "(on ") } +// TestEvalOfBoundNamespaceMembersThroughCLI: `-e` reads usages a namespace-owned +// binding joins through the binding's class, so the valued end's declared value +// is the class's one object rather than a second construction. +func TestEvalOfBoundNamespaceMembersThroughCLI(t *testing.T) { + binary := buildCLI(t) + const model = `package P { + part def Car; + part x : Car; + part y : Car = new Car(); + bind x = y; +} +` + got := check(t, binary, model, "-e", "P::x", "-e", "P::y", "-e", "P::x === P::y") + wantReport(t, got, 0, "= Instance(ID: 1)", "= true") +} + // TestCheckOfInheritedConstraintAfterInstantiate checks what `-instantiate p // -constraint C` promises: the verdict, and so the exit status a build step reads, // is about the object of p rather than about C's declared defaults. diff --git a/docs/guide/06-behavior.md b/docs/guide/06-behavior.md index 2c4fefe5b3..1837e7330e 100644 --- a/docs/guide/06-behavior.md +++ b/docs/guide/06-behavior.md @@ -346,7 +346,9 @@ and their bodies may hold whatever an action body holds: a flow of nodes joined (`first start; then …`; every node no succession leads to starts with the behavior, unordered), forks, joins and decisions, timed and signal accepts, sends, nested action nodes with flows of their own, and typed usages with pin bindings (`do action poll : Poll { inout n = ticks; }`). A -body stating no flow still runs its statements in declaration order. A braced block without the +body stating no flow leaves the statements no `then` relates unordered: `declared` takes them in +declaration order, and `explore` and `-engine check` reach every order +([below](#seeing-the-whole-outcome-set-explore)). A braced block without the keyword — `entry { … }`, `do { … }`, `exit { … }`, and a transition's `do { … }` — is one anonymous action with that body, the same as `entry action { … }`: an attribute declared inside the block is local to it and shadows the state's, and a `terminate;` in it ends the whole block @@ -768,7 +770,19 @@ tried. Under `explore` an action step is one token advancing one node — not, a policies, every steppable token moving once — so the picks fall in consecutive steps and a branch of several nodes can run ahead of, or be overtaken by, a concurrent one at each of them. A `complete` exploration therefore covers every interleaving of the nodes the library leaves -unordered, at body granularity: the statements of one body run without interruption. A run that +unordered. Where another performance's moves can change what a leaf body computes, the body +yields after its initial values are read and after each statement, so a concurrent branch may +run between a body's snapshot `attribute t : Integer := c` and its `assign c := t + 1`; the +statements of a nested body run first to last, and an action definition's own statements in +the token order each policy gives them (`reverse` last to first). A calc or constraint body is +performed whole inside the step that evaluates it: no other performance runs between its +statements, but the statements no `then` relates are unordered among themselves, so `explore` +and `-engine check` reach every order of them (a calc whose `y := y * 10` and `y := y + 2` are +unordered returns `12` or `30`), and its result expression or condition is evaluated after +them; `declared` and `reverse` run calc and constraint bodies in declaration order. A case body +stating no succession likewise leaves its steps unordered under `explore`, `-engine check`, +replay and seeded schedules, while `declared` and `reverse` perform them in declaration order. +A run that fails under some order is an outcome of its own (`error: …`), not the end of the exploration; a behavior with no choice point explores in exactly one run (`no choice points` in the witness column); the same model explores to the same table every time. With `-trace`, the diff --git a/docs/internals/design/region-order-scheduling.md b/docs/internals/design/region-order-scheduling.md index 2a1eae07c1..1dc5d2e358 100644 --- a/docs/internals/design/region-order-scheduling.md +++ b/docs/internals/design/region-order-scheduling.md @@ -251,7 +251,7 @@ choice at t=0.0: next do top, dispatch AnotherSignal (unordered; ran do top firs `%step`, `%continue` and `%advance` count them as they count every choice (`2 choice points; %trace on to see them`), `%choices` lists them, and over gRPC and Connect each is the informational `choice-point` diagnostic placed at the state or transition drawn. The -self-model's `choiceKindCount` counts nine kinds. +self-model's `choiceKindCount` counts thirteen kinds. ### Witness lines and replay diff --git a/docs/internals/design/scheduling.md b/docs/internals/design/scheduling.md index c82375be6c..5f8d06fef5 100644 --- a/docs/internals/design/scheduling.md +++ b/docs/internals/design/scheduling.md @@ -23,7 +23,7 @@ linearization a run took, the points at which it had a choice, and the rule it c ## Choice points (`choice.go`, `action_choice.go`) A `ChoicePoint` is one point where an executor had several enabled alternatives the library leaves -unordered and took one by its scheduling rule. `ChoiceKind` names the ten: +unordered and took one by its scheduling rule. `ChoiceKind` names the twelve: | Kind | Where it is noted | Alternatives, canonically | |------|-------------------|---------------------------| @@ -35,6 +35,8 @@ unordered and took one by its scheduling rule. `ChoiceKind` names the ten: | `ChoiceEntryOrder` | `enterRegionsInto` (`entering `), `enterForkBranches` (`fork `), a history's restore | the next entry unit of each region, branch or restored region of a composite state, in declaration order; the one performed first is taken. A unit performing no behavior rides with the performing unit beside it | | `ChoiceEntryStep` | `StateExecutor.entryStep` (`entry at t=…`) | a free dispatch due at the instant, followed by the held entry cascades that can resume; the free dispatch is listed first and the selected alternative is taken | | `ChoiceExitOrder` | `exitState` (`exiting `) | the next exit unit of each region a state leaves, innermost first within a region, in declaration order; the one performed first is taken | +| `ChoiceStepOrder` | `StateExecutor.chooseStepOrder` | the move of a due do behavior and a dispatch due beside it; the one that goes first is taken | +| `ChoiceStatementOrder` | `stmtEngine.pickStatement` (`statements in `) | the statements of one body that no succession or control structure orders and that may run next (`lower.StatementOrder`), by declaration position; the one run first is taken | | `ChoiceDueOrder` | `Context.runDue` (`advance.go`); the checker's run, one executor holding the turn until it has no move at the instant | the executors due at one instant, in creation order; the one run first is taken | | `ChoiceDispatchOrder` | `StateExecutor.nextEvent`, when the queue leaves several events unordered at its head | the events due at one instant the library does not order — time triggers with each other, a time trigger with a pool event of the same timestamp — labelled as the queue labels them; the one dispatched first is taken. A completion event goes before any of them and pool events keep their arrival order (`earlierFirstIncomingTransferSort`), so neither is a choice | @@ -121,6 +123,85 @@ interaction the multiplicity checker cannot support, is refused before the node two-reading treatment of unwritten succession ends is documented in the [semantic oracle](../../project/behavior-semantic-oracle.md#repeated-action-steps-and-shared-writes). +## Leaf action-body interleavings + +A leaf action body — a node's initial feature values and its direct statements, with no flow of +its own — runs through the statement engine (`usageWork.perform`, `stmtEngine`). Its performance +encloses separate times the [semantic +oracle](../../project/behavior-semantic-oracle.md#a-leaf-bodys-start-shot-and-its-assignments-another-performance-may-run-between-them) +derives: the start shot, where `attribute t : Integer := c` snapshots `c`, and one subperformance +per statement, an `assign` writing when it ends. Another performance may run between any two. + +- **Atomic units.** One initialization (every initial value of the body, read at its start + shot) and one statement are each one move: an assignment reads its value and writes it with + no boundary between, since the library places nothing between them. A loop iteration and a + step of a flow the body drives are one move each, as they already were. +- **Scheduler boundaries.** Where a body divides, the run yields after its start shot and after + each statement (`Context.bodyPerformed`, `Context.yieldBody`, `bodyPause.yielded`): the token + goes back to the step's steppable tokens with its body frame held, and the next pick among + them is an ordinary `ChoiceTokenOrder`. No new choice kind is added, so a witness, a replay + file, a snapshot and a held image name a body boundary as they name any token move, and the + body frame is resumed where it paused. +- **Where a body divides.** `lower.BodyDivides` lists a body's moves (its start shot, then each + statement, a conditional's guard by itself and a loop counted twice) with their footprints + (`Footprint`, the reduction's), and the footprints of every node of the outermost flow and its + nested flows; the node's own, with its per-performance pins removed, when it may run beside + itself. When the flow can hold two tokens at once and two or more of the body's moves are + dependent on one of those footprints, the body divides. With at most one, every interleaving + inside the body only reorders independent moves, so the body stays one move and the state + space does not grow. +- **Guards.** An `if`'s guard is evaluated before its branch (`IfThenPerformance`), so a dividing + body yields between them (`Context.guardPerformed`); the branch taken is kept in its + `branchFrame` across the pause. A state's do body yields there only under one-move schedules, + so `reverse`, `declared` and seeded runs keep their state traces. +- **Do bodies stating a flow.** A `for`, `while` or `if` node of a do body's stated flow + yields after each iteration and branch statement, as the same statement in a statement-list + body does (`bodyRun.nodesYield`); the flow counts the node's yield as a move + (`ActionExecutor.yieldedIn`), so adding `then` takes no interleaving away. A token of a flow + such a node drives moves only through that node's work, never as a move of the outer flow + (`Token.drivenUnder`), so a replayed witness meets the same choices it recorded. +- **Callees in executors of their own.** An action a step is typed by, or a body performs, runs + in its own `ActionExecutor`, outside the graph `BodyDivides` reads. When the body or flow + driving it goes one move at a time, the callee's start shot is a boundary + (`Context.startShotMove`), its own bodies divide where two of their moves may touch what they + do not hold (`lower.BodySharesMoves`), and its flow pauses after each move when two of its + moves may (`lower.FlowSharesMoves`, `Context.tokenStepBody`); the attributes and `in` parameters + the callee's definition declares are its performance's own, not shared, while each write to an + output reaches the caller's pin and its streaming flows as it is made, so outputs are shared. A pause inside a body-driven + token propagates to the body driving it. A seeded run whose step holds another token pauses + there when `drawYield` says so (`Context.drawsTokenSteps`), so seeds reach those outcomes too. +- **Executors on one clock.** Separate executors — object behaviors, state machines, actions + started together — still interleave by whole turns: the executor the due order draws runs + until it has no move at the instant (`Context.runDue`, `invocationRun.turn`). Their moves + within one instant are not interleaved, which spec compliance records as approximate. +- **Ordered and unordered statements.** A succession inside a nested flow orders its steps, so + no boundary reorders them; a boundary lets only another performance in. The direct statements + of one body that no succession or control structure orders are unordered + ([derivation](../../project/behavior-semantic-oracle.md#direct-statements-of-one-body-no-succession-orders-each-is-performed-in-which-order-is-open)). + `lower.StatementOrder` lists, before each statement runs, those that may run next: a statement + `then` links waits for the one before it, a local declaration, `return` or `perform` keeps its + place, and two orders that only swap adjacent independent statements (by the reduction's + footprints) are one. Two or more candidates are a `ChoiceStatementOrder` + (`stmtEngine.pickStatement`); a compound statement already started yields at its inner + boundaries so a dependent sibling may run between them. `declared` and `reverse` run + a nested body's statements first to last as before, and an action definition's own statements + in the token order each policy already gave them (`reverse` last to first), so their results + do not move; `explore`, the checker, replay and seeds + choose. A terminate action usage's implicit terminate is one of its body's statements in this + sense. `explore` enumerates without partial-order reduction, so a body of many dependent + unordered statements may exhaust its run budget, which it reports as incomplete. +- **Policies.** `explore`, replay and the checker step one move at a time and yield at every + boundary of a dividing body. `reverse` and `declared` never yield, so a body runs whole as + before and their results do not change. A seeded run whose step holds another token yields + at a boundary when `scheduler.drawYield` — a hash of the seed, the token's ID and the + boundary's ordinal — says so. That draw does not consume the generator the run's picks + come from, so one seed still reproduces one run. +- **Checker and SMT.** The checker's moves are the executor's, so it reaches every interleaving + exploration does, under its reduction and state merging; a paused body frame is part of the + state it keys. The SMT encoding performs a body as one move and refuses a flow whose body + divides (`Flow.checkBodyInterleaving`, `UnsupportedError` construct `body interleaving`), so + it reports such a flow as not covered rather than encoding one order. + ## Policies (`scheduler.go`) A `SchedulePolicy` is parsed from one spelling and printed back to it: @@ -129,7 +210,7 @@ A `SchedulePolicy` is parsed from one spelling and printed back to it: |----------|----------------------|--------------------------------------|-----------| | `reverse` (default, zero value) | reverse spawn order | first in declaration order | last created | | `declared` | spawn order | first in declaration order | first created | -| `seed:` | shuffle of the tokens not parked | uniform draw | uniform draw | +| `seed:` | shuffle of the tokens not parked; a dividing body yields where the seed's hash says | uniform draw | uniform draw | | `replay:` | the witness's `step n: …` line, then `reverse`'s pick alone | the witness's line, then `reverse` | the witness's line, then `reverse` | | `explore[:runs=N,depth=D]` | the exploration's plan | the exploration's plan | the exploration's plan | diff --git a/docs/project/README.md b/docs/project/README.md index d7e9e1a219..d085df8d9a 100644 --- a/docs/project/README.md +++ b/docs/project/README.md @@ -64,6 +64,9 @@ within these records and means nothing outside this repository. can give it, adjudicated against the specification, the Kernel Function Library, the corpora and the pinned pilot, and closed abstract-only: the runtime keeps its typed refusal and the checker warns on every use +- **[Constraint-body steps and the `all T` extent](constraint-body-steps.md)** — what a + constraint body's statements and the extent of a type mean, derived from KerML and SysML, what + the runtime executes, and what it leaves tool-defined or refuses - **[Exception handlers](exception-handlers.md)** — whether SysML v2 spells an exception handler or exception propagation, adjudicated against the specifications, the library, the corpora and the pilot, and closed as not a SysML v2 construct, with the idiom that covers the need — a diff --git a/docs/project/behavior-semantic-oracle.md b/docs/project/behavior-semantic-oracle.md index 8f59273eac..b4f0445811 100644 --- a/docs/project/behavior-semantic-oracle.md +++ b/docs/project/behavior-semantic-oracle.md @@ -77,6 +77,9 @@ 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 | +| `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 | | `Actions.sysml` `ControlAction` | `bind start = done` — "A ControlAction is instantaneous" | A control node adds no duration; its successor may start as soon as its predecessors end | | `Actions.sysml` `ForkAction` | "Fork behavior results from requiring that the target multiplicity of all outgoing succession connectors be 1..1" | Each fork performance is followed by exactly one performance of every target | | `Actions.sysml` `JoinAction` | "Join behavior results from requiring that the source multiplicity of all incoming succession connectors be 1..1" | Each join performance follows exactly one performance of every source, one per incoming succession | @@ -466,10 +469,188 @@ subaction as a token started with the owner's performance (`ActionGraph.Concurre `StartFlow`), so its interleavings are the same choice points fork branches are. The exact golden records the default schedule. -Not covered: a nested action node whose members are only statements, and a loop, branch or -behavior body stating no flow, run their statements and nodes in declaration order, as they did -before; the library orders them no more than it orders `a` and `b`, so that order is the -executor's choice, recorded in [spec compliance](spec-compliance.md). +The statements of a nested action node whose members are only statements, and of a loop, branch +or behavior body stating no flow, are ordered no more than `a` and `b` are; the sections below +derive the interleavings another performance may take between them and the orders among them. + +### A leaf body's start shot and its assignments: another performance may run between them + +Fixtures: `action_explore_body_lost_update`, `action_explore_body_three_way`, +`action_explore_body_fork_lost_update`, `action_explore_body_ordered_substeps`, +`action_explore_body_guard_branch`, `action_explore_body_typed_callees` and +`action_explore_body_performed_callees` (golden, explored), with +`action_step_multiplicity_single_assignment` (one outcome). + +``` +Race c := 0; start → a[2] { t := c; assign c := t + 1 } → done +ForkPlain c := 0; start → f ⇉ a { t := c; assign c := t + 1 } ─┐ + ⇉ b { t := c; assign c := t + 1 } ─┴→ j → done +``` + +Derived constraints: + +- `attribute t : Integer := c` is an initial feature value (KerML 1.0 `FeatureValue`, + `isInitial`): `t` takes the value `c` has at the start of the performance of `a` that features + it, its `startShot` (`Occurrences.kerml`). It is a snapshot; a later write of `c` does not + change `t`. +- `assign c := t + 1` is an assignment action usage, one of `a`'s `assignments`, so one of its + `subactions` (`Actions.sysml`): a subperformance, enclosed in `a`'s performance + (`Performances.kerml`), not coincident with its start. Its type `AssignmentAction` is a + `FeatureWritePerformance`, which writes `c` "at time its performance ends" + (`FeatureReferencingPerformances.kerml`). The read of `c` and the write of `c` are two times + of one performance of `a`. +- No `HappensBefore` links the two performances of `a[2]` (repeated performances of one step, + [above](#repeated-action-steps-and-shared-writes)) nor the two fork branches `a` and `b` + (`ForkAction`). So the other performance's start shot may fall between this one's start shot + and the end of its assignment. When both start shots come before both writes, both read `0` + and both write `1`. +- The library does not divide one assignment further: `FeatureWritePerformance` states only that + the write happens when the assignment ends, and nothing places the evaluation of its value + expression at another time. The tool keeps an assignment one move, its value read and its + write together, as it keeps one initialization. That is the tool's reading where the library + is silent, not a library constraint. So `a[3] { assign c := c + 1; }` reads and writes `c` in + one move and loses no update (`action_step_multiplicity_single_assignment`, `c = 3`). +- A succession inside a body is a `HappensBefore` link. In `action_explore_body_ordered_substeps` + the branch `a` performs `a1` then `a2`, each appending to `log`, and the branch `b` appends + `"b"`. `b` may run before `a1`, between `a1` and `a2`, or after `a2`, but `a2` never runs + before `a1`. + +- An `if` is an `IfThenPerformance` (`ControlPerformances.kerml`): `succession [1] ifTest then + [0..1] thenClause`, so its guard is evaluated before, not with, its branch. In + `action_explore_body_guard_branch`, `a[2] { if c < 1 { assign c := c + 1; } }`, both + performances may read the guard before either assigns: `{c = 1, c = 2}`. +- A step typed by an action definition (`action a : Inc`) and an action a body performs + (`perform action pa : Inc`) are performances of `Inc` like any other, whatever executor runs + them: `Inc`'s start shot and its assignment are two times, and nothing orders the other + branch's performance between them. With `Inc` reading `counter.c` into `t` and writing + `t + 1`, both branches admit `{seen = 1, seen = 2}` (`action_explore_body_typed_callees`, + `action_explore_body_performed_callees`). + +Open: where the other performance runs relative to this performance's start shot and its +assignments. + +Pinned outcomes: `Race` `{c = 1, c = 2}`; `a[3]` with the same body `{c = 1, c = 2, c = 3}` +(`action_explore_body_three_way`); `ForkPlain` `{c = 1, c = 2}`; the ordered substeps +`{log = "12b", log = "1b2", log = "b12"}`. Each is stated as `outcomes` citing this section, with +a `.trace.order` where the library fixes an order. Exploration reaches every member and nothing +else: `Race` in 6 linearizations, `a[3]` in 90, `ForkPlain` in 6. The exact goldens record the +default schedule. + +The executor gives a leaf body a scheduler boundary after its start shot and after each statement +where another performance's move could change the outcome there: when two or more of the body's +moves are dependent on a move of a performance that may run concurrently (`lower.BodyDivides`, +over the footprints the checker's reduction uses). A body with at most one such move runs as one +move, because every interleaving inside it only reorders independent moves. The order of the +statements of one body is open as well, as the next section derives. A performance invoked in an +executor of its own, under a body or a flow driven one move at a time, is analysed by its own +flow: its start shot and each move that may touch what it does not hold (`lower.BodySharesMoves`, +`lower.FlowSharesMoves`) are boundaries too. The attributes and `in` parameters its definition +declares are its performance's own, so moves touching only them are not boundaries +(`action_explore_body_own_callees`: one outcome in two runs). Its outputs are not: each write to +one lands at the invoking node's pin and goes on along its streaming flows as it is made, so a +performance beside it may read the pin before, between or after two writes +(`action_explore_body_callee_outputs` under `testdata/robustness`: `seen` is 0, 1 or 2). + +Not covered: object behaviors, state machines and actions run by separate executors on one +clock interleave by whole turns. The executor drawn to run at an instant runs until it has no +move left there, so their moves at one instant are not interleaved. `spec-compliance.md` records +this as approximate. `-engine smt` encodes a +body as one move, so it reports a flow with a dividing body as not covered (`body interleaving`) +instead of encoding one order. + +### Direct statements of one body no succession orders: each is performed, in which order is open + +Fixtures: `action_explore_statement_order_dependent`, `action_explore_statement_order_chain` and +`action_explore_statement_order_if` (golden, explored), with +`action_explore_statement_order_independent` and `action_explore_statement_order_then` (each one +outcome). + +``` +Order s { assign x := 1; assign y := x; } +Chain s { assign x := x + 1; assign x := x * 2; assign x := x + 3; } -- x := 1 first +``` + +Derived constraints: + +- Each direct statement of a body is an action usage of it: an `assign` one of its + `assignments`, a `send` one of its `sendSubactions`, an `if` one of its `ifSubactions`, a + `while` or `for` one of its `loops`, each a subset of `subactions` (`Actions.sysml`). Each is a + subperformance enclosed in the body's performance, as + [the subactions above](#subactions-no-succession-orders-each-is-performed-during-the-owner-in-which-order-is-open) + are, and nothing written between two statements links them by `HappensBefore`. So in `Order` + `y` may read `x` before or after `x` is written: `{y = 0, y = 1}`. +- `then` written before a statement is a succession from the statement before it + (`action_explore_statement_order_then`): `assign y := x` follows `assign x := 1`, `y = 1`. The + library's own `ForLoopAction` orders its assignments the same way. +- A control structure orders what it contains: an `if`'s guard precedes its branch + (`IfThenPerformance`, `ifTest then thenClause`) and a loop's iterations follow each other + (`LoopAction`). The `if` itself is one subaction of the body and is unordered against its + siblings: in `action_explore_statement_order_if` the assignment of `x` may come before the + guard reads it, `{y = 0, y = 10}`. +- Two statements that neither read nor write what the other writes, and touch no message or + control the other does, commute: every order of them reaches one result + (`action_explore_statement_order_independent`). + +Open: the order of two statements no succession or control structure orders. + +Pinned outcomes: `Order` `{y = 0, y = 1}`; `Chain` `{x = 6, x = 7, x = 9, x = 10}` over its six +orders; the `if` case `{y = 0, y = 10}`. Each is stated as `outcomes` citing this section; +exploration reaches every member and nothing else, `Order` in 2 runs, `Chain` in 6, the +independent body in 1. The exact goldens record the default schedule. + +`declared` and `reverse` perform a nested body's statements first to last, and an action +definition's own statements in the token order each policy gives them (`reverse` last to first), +a tool-defined order. +`explore`, `-engine check`, replay and seeded schedules choose among the statements that may run +next (`lower.StatementOrder`). Two orders that differ only by swapping adjacent independent +statements, by the footprints the checker's reduction uses, are one choice. A declaration of a +local feature or usage, a `return` and a `perform` keep their place, because the statements after +them read what they declare. `-engine smt` reports a body with two dependent unordered +statements as not covered (`statement order`) instead of encoding declaration order. + +### A terminate action usage's body and its implicit terminate: each is performed, in which order is open + +Fixtures: `action_terminate_usage_body_ends_itself`, `action_terminate_usage_with_body`, +`action_terminate_usage_body_performs_action`, `action_terminate_usage_in_block_names_itself`, +`action_terminate_usage_binds_output_pin` and `action_terminate_usage_body_ends_itself_binds_pin` +(explored). + +``` +stop terminate { out code : Integer; assign code := 42; then terminate; then assign other := 7; } +bind stop.code = result; -- result : Integer [1] +``` + +Derived constraints: + +- A terminate action usage is a `TerminateAction` (`Actions.sysml`), an ordinary `Action` whose + body is an `ActionBody` (SysML.xtext `TerminateActionUsage`), so its body states successions as + any action body does: `then` orders the statements it links, as in + [the section above](#direct-statements-of-one-body-no-succession-orders-each-is-performed-in-which-order-is-open). +- `TerminateAction` declares `action terminateOccurrence : destroy[1]`, the subaction that ends + the terminated occurrence. Nothing in the library or in the body links it to the body's other + subactions by `HappensBefore`, so it may be performed before, between or after them; the + occurrence it ends is the enclosing performance, and what the body has not performed by then is + not performed. +- A `terminate` written in the body ends the usage's own performance at that point of its chain: + the statements after it are not performed. +- An output pin the body has not written when the terminate falls holds no value. A binding of it + to a feature of multiplicity `[1]` then binds no value, which the run reports as the typed + multiplicity violation it is for any unvalued binding. + +Open: where the implicit terminate falls among the body's statements. + +Pinned outcomes: each fixture states as `outcomes` every result of the terminate falling before, +inside or after the body's chain; the two pin fixtures list the multiplicity violation as an +`error` outcome. Exploration reaches every member and nothing else. + +`declared` and `reverse` perform the implicit terminate after the body, a tool-defined +linearization; `explore`, `-engine check`, replay and seeded schedules choose it against the +body's statements, between the steps of a `then` chain included. + +Approximate: action usages written in an `if` or loop block with no succession between them +(`action_terminate_usage_in_block_binds_pin`'s `stop` and `later`) are performed in declaration +order under every policy, so the outcome of `later` running before `stop` ends the block is not +reached. ### A write between two nodes of a concurrent branch: three orders, three outcomes diff --git a/docs/project/constraint-body-steps.md b/docs/project/constraint-body-steps.md new file mode 100644 index 0000000000..382121868b --- /dev/null +++ b/docs/project/constraint-body-steps.md @@ -0,0 +1,143 @@ +# Constraint-body steps and the `all T` extent + +This record derives, from KerML 1.0 (formal/2026-03-01) and SysML v2 Part 1 (formal/2026-03-02), +what two runtime surfaces must do: a constraint body that states action statements before its +result expression, and the extent expression `all T`. Library text is quoted from the bundled +copies under `internal/workspace/libs/stdlib/`. UML, fUML and PSSM are not cited as authority; +nothing below rests on them. Where the specifications are silent the record says so and names the +rule the runtime applies in their place as tool-defined. The compliance rows that point here are +the *constraint body's action statements* row and the *extent expressions* row of the +[compliance mapping](spec-compliance.md). + +## 1. Statements in a constraint body + +### What a constraint body is + +- **SysML v2 §7.20.2.** A constraint definition is a KerML predicate and a constraint usage a + KerML Boolean expression; "the body of a constraint definition or usage is notated like a + calculation body (see 7.19.2), except that the result expression must be Boolean". +- **SysML v2 §7.19.2.** A calculation body is notated like an action body, with an optional + result expression at the end; the body's members are features — and steps — of the + calculation. +- **KerML §7.4.8.1–§7.4.8.2.** A function is a behavior, so its body's steps are steps of every + performance of it; "the result expression ... is implicitly bound to the result parameter". +- **KerML §7.4.8.4, §8.4.4.8.1.** A predicate is a function whose result is Boolean; each + evaluation of a constraint is one performance of it, and the verdict is that performance's + result (`checkFunctionResultBindingConnector`). +- **SysML v2 §8.4.16.1–§8.4.16.2; `Constraints.sysml`.** A constraint usage is checked through + `ConstraintCheck`: an asserted constraint is one whose every performance's result is true, + a negated one whose every result is false. + +So a constraint body stating `attribute y : Real = 0; assign y := 5; y > 3` is one Boolean +function: `y` is a feature of each performance, the assignment is a step of it, and the verdict +is the value of `y > 3` in that performance. + +### What the specifications settle + +1. **The steps are performed for the verdict.** They are steps of the function (KerML §7.4.8), + so evaluating the constraint performs them; a verdict that skipped them would be the result of + a different function. +2. **Locals belong to the performance.** A feature declared in the body is a feature of the + performance (KerML §7.4.7.1, steps and features of a behavior are featured by its + performances), so each check has its own values and nothing carries over between two checks. +3. **An untargeted assignment writes the constraint's own performance.** SysML v2 §7.17.9: an + assignment "sets the value of a referent feature of a target occurrence"; with no target + written, the target is the default. `Actions.sysml` declares + `in target : Occurrence[1] default that as Occurrence` on `assignmentActions`, documented + "the default target for assignmentActions is its featuring instance (if that is an + Occurrence)" — here the constraint performance. SysML v2 §8.3.17.5 + (`AssignmentActionUsage::targetArgument`, `referent`) makes the referent a feature *of that + target*. An untargeted `assign mass := …` where `mass` is a feature of the constrained part, + not of the performance, therefore names no feature of its target. +4. **The constraint's parameters are features of the performance.** A write to one changes that + performance's value of it, not the argument expression or the caller's binding. + +### What they do not settle + +- **Order of steps relative to the result.** KerML binds the result parameter to the result + expression's result (§7.4.8.2, §8.4.4.8.1); it does not sequence the result expression after + the other steps. Steps with no succession between them are unordered (KerML §7.4.7.2; the + Systems Library's `subactions` are subperformances with no implied order). The result + expression is unnamed, so no `then` can name it either. For + `{ attribute y := 0; assign y := 5; y > 3 }` the specifications do not say whether `y > 3` + reads 0 or 5. +- **Effects outside the performance.** An explicit target (`assign v.mass := …`), a `send`, + a `perform` of another action, or a `terminate` acts on occurrences other than the + performance. The specifications allow writing them in a calculation body and so in a + constraint body; they do not say how a *check* — which tools run repeatedly, speculatively and + in solvers — relates to those effects. + +### What the runtime does + +- **Tool-defined order.** The body's steps run in declaration order — inherited bodies first, + in the order the specialization chain gives them, then the usage's own — and the conditions + (the result expression and the nested `require`, `assume` and `assert constraint` members) are + evaluated afterwards, in the state the steps left. This is a linearization the specifications + permit but do not prescribe. +- **Locals and parameters** live in one fresh frame per check; the constraint's parameters are + copied into it, so writing one changes this check's copy only. +- **Refused, with typed errors, until the open points above are decided:** + - an untargeted assignment to a name that is no feature of the performance + (`ErrConstraintExternalAssignment`, by point 3 above, not a policy choice); + - explicit, chained or qualified assignment targets, `send`, `perform`, and `terminate` + (`ErrConstraintEffect`); + - stated flow: `first`/`then` successions and control nodes in a constraint body + (`ErrStatementNotExecutable`), since honouring them would need a decision on how they + combine with the unordered result expression. +- **Analysis and verification cases** keep their own procedure: their steps are the case's + action flow, not a constraint body, and are unchanged. +- **The solver** does not encode a body's steps. A condition whose body states steps is refused + as not translatable (`constraint body steps`); it is never translated as if the steps were + absent. + +## 2. The `all T` extent + +### What the specifications say + +- **KerML §7.4.9.2.** `all T` "evaluates to a sequence of all instances of the named type". +- **KerML §7.3.2.1.** The set of things a type classifies is its extent. +- **KerML §9.4.2.** `abstract function 'all' { return : Object[0..*]; }` — the result is not + declared `ordered`; KerML gives the sequence no order. +- **KerML §7.4.6.3.** A binding connector makes the values of its ends the same: two usages a + binding joins denote one object. +- **KerML §7.3.4.4.** A feature's values include those of the features that subset it. +- **SysML v2 §7.6.3.** A usage owned by a package has default multiplicity `0..*`. + +### Where the model determines the extent + +An instance belongs in `all T` when the model forces it to exist: a usage with a lower bound of +at least one whose featuring instance exists (the package-level object usages the run +materializes, and the composite features of existing objects, their own required features +recursively), objects created or written by the behaviors run so far, and the variants of a +variation. Three places where the runtime used to differ from what the model determines are +corrected: + +1. **Namespace-owned bindings.** `bind a = b;` written in a package now makes `a` and `b` one + object; a usage bound to a feature chain denotes the chain's object; ends with values of + their own must agree (`BindingConflictError`). Bindings join usages into one equivalence + class wherever in the model they are written. +2. **Held performances and connections.** Action, state, connection, interface, allocation and + flow usages an object holds are reached by the extent walk, as its parts are. Constraint, + requirement and calculation usages are not: reading one evaluates it rather than reaching an + occurrence it holds. +3. **Namespace-level subsetting.** A package-level collection usage's objects include those of + the usages subsetting it; anonymous members make up only the remaining lower bound, an + abstract usage has none of its own, and a count outside the declared multiplicity is + `ErrMultiplicityViolation`. + +Destroyed objects are excluded; each object appears once; library-declared object usages +(`Time::universalClock`) are instances like any other. + +### The tool-defined boundary + +- **Order.** The specifications give none (§9.4.2). The runtime answers a deterministic + order — document name, then declaration order, as the extent row of the compliance mapping + states — so two runs over the same model and the same behavior answer the same sequence. +- **Instances no model element determines.** A package-level `part c : Car;` has multiplicity + `0..*` (SysML v2 §7.6.3): the single object the run materializes for it is the runtime's + choice, as is the object of a valueless `ref`, and the number of members of an open `[1..*]` + beyond its lower bound. The runtime materializes the lower bound (one for a package-level + object usage), and the extent reports those objects. A model that needs a specific count + states it with a multiplicity. +- **Unbounded types.** The extent of a data type other than an enumeration (`all Integer`) is + refused rather than enumerated. diff --git a/docs/project/pilot-differential-baseline.json b/docs/project/pilot-differential-baseline.json index 60fdc0a124..ef52cbc1b9 100644 --- a/docs/project/pilot-differential-baseline.json +++ b/docs/project/pilot-differential-baseline.json @@ -61,7 +61,7 @@ "dir": "examples", "origin": "ours", "files": 46, - "digest": "sha256:03e591d198672257e12e4744891297f08486b511a4e01e8d0921ee83d2bcff80" + "digest": "sha256:7d4322cf546559c3c82fe63bb450753c3f0634288c21b59a38aa66ea66cd0638" }, { "name": "probes", @@ -71,7 +71,7 @@ "digest": "sha256:b0153c55bbfcdacabab911725dc44e0e7f4d3a501a281b1f3f2a13cda19737c6" } ], - "recorded": "2026-10-03" + "recorded": "2026-10-04" }, "totals": { "files": 382, diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 775c1e4a39..470fb15a46 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -254,7 +254,7 @@ file disagree. Edit neither block in place: regenerate them. | Identity (`===`, `!==`), null coalescing (`??`, lazy) and remainder (`%`) evaluated at runtime | `eval.go` `evalIdentity`/`evalNullCoalesce`/`evalArithmetic` | `calc_identity_operators.sysml`, `calc_null_coalesce.sysml`, `calc_modulo_operator.sysml` | ✅ Faithful | | Value classification evaluated at runtime (`istype`, `hastype`, and `x @ T` with a value subject and an ordinary type, the third `ClassificationTestOperator` of the KerML 1.0 ClassificationExpression grammar beside `hastype` and `istype`; KerML 1.0 §7.4.9.2: `istype` and `hastype` hold when every value of the operand is classified, `@` when at least one is; SysML v2 8.4.4.2 ClassificationExpression) | `eval.go` `evalTypeClassification`, `valueHasType`, `directValueType`, `runtime/classification.go` `classifyValue` (the one reading of "the value is of type T" that `as` and a feature write share), and `evalOperator` routing `OpAt` there through `classifiesValue` — `x @ T` is a value test when a subject is written and T resolves to a type that is not a metadata definition or metaclass (`semantics.IsMetadataType`), and the metadata test of the next row otherwise — a value's *direct* type is derived from what the value already holds (a scalar constant's representation — an integer is an `Integer`, a finite real a `Rational` whatever number it holds, an infinity a `Real`, a Complex a `Complex` on the real axis or off it (`representationPrim`; KerML 1.0 §8.4.4.9.2: only the rationals have a finite literal, so a `LiteralRational` is classified in `Rational`) — a string's `String`, an object's classifier, a selected variant, an enumeration literal's enumeration, a quantity's numeric value), so no type field was added to `runtime.Value`; `istype` is `classifyValue` by any type (`byAnyType`) — the operand's representation and declared types conforming to the target or classifying it as a composed type does, the same relation a cast and a feature write ask — and `hastype` is `classifyValue` by the value's own type alone (`byOwnType`), so `rat : Rational = 4` answers `rat hastype Integer` `true` and `rat hastype Rational` `false`; a target the value's types leave open (`Natural` against a bare integer, `Even :> Integer` against `5`) is `false` for `istype`, since nothing states the value is one — unless the target is an enumeration, whose enumerated values are the only instances it has (SysML v2 §8.3.7 EnumerationDefinition: "An EnumerationDefinition is an AttributeDefinition all of whose instances are given by an explicit list of enumeratedValues. This is realized by requiring that the EnumerationDefinition have isVariation = true, with the enumeratedValues being its variants"), so membership is decided by equality with them (`classifyNarrower` → `enumeratedValue`, over `semantics.Model.LiteralsOf` and each literal's evaluated value): with `enum def Level :> Integer { low = 1; high = 3; }`, `3 istype Level` and `3 @ Level` are `true`, `2 istype Level` `false`, and a plain `enum def Color { red; green; blue; }` classifies by identity with its literals (`3 istype Color` false, `Color::red istype Color` true, no other enumeration's literal is a `Color`). `hastype` does not infer an enumeration for a bare scalar: §7.4.9.2 reads the value's direct type ("just directly", the pinned reference's release notes), a bare `3` is directly an `Integer` and nothing else, so `3 hastype Level` is `false` and `3 hastype Integer` `true`; an enumeration literal's own type is its enumeration, so `Level::high hastype Level`, `lvl hastype Level` for `lvl : Level = Level::high` and `(3 as Level) hastype Level` are `true` and `Level::high hastype Integer` `false` (`Integer` is a supertype, so `Level::high istype Integer` is `true`) — a scalar-valued literal evaluates to its assigned scalar (`eval.go` `enumLiteralValue`, so `Level::high == 3` and `Level::high + 1 == 4`) carrying the literal's identity in its payload (`value.go` `Value.ofLiteral`/`EnumerationLiteral`, no field added), and `valueTypes` reads that identity before the library scalar lookup, as do the readers of a literal as an occurrence — a chain through it (`chainMemberValue`, `assign_chain.go` `chainObject`: `Level::high.n`) and the element a metadata test classifies (`elementDenotedBy`: `Level::high @ Hot`) — so a scalar-valued literal answers them as an unvalued one does — and identity (`===`, `valueIdentical`) tells the literal from the bare scalar it equals and from another enumeration's literal of that value, as it tells an Integer from an equal Real (`Level::high === 3` is `false`, `Level::high == 3` `true`); a value equal to none of an enumeration's literals whose literal values cannot be evaluated is that error, not `false` — and a sequence or set is classified elementwise — `valuesClassified` asks every element for `istype` and `hastype` and any element for `@`, so an empty value is `true` under the first two and `false` under `@` (§7.4.9.2) | `eval_operator_test.go:TestTypeClassificationOperators` (the reference's 20-case truth table case by case), `:TestTypeClassificationFollowsSelectedVariant`, `conformance/w7d_type_classification.sysml` + `.expected.json` (the same table over the library scalar types), `robustness_test.go:type_classification_unresolved_type`, `:type_classification_undetermined_value_type`, `:enumeration_typed_feature_holding_an_unenumerated_value`, `:enumeration_whose_literal_value_cannot_be_evaluated`, `classify_test.go:TestEnumerationClassifiesByItsEnumeratedValues`, `:TestEnumerationTypedFeatureAdmitsOnlyEnumeratedValues`, `conformance/enumeration_value_classification.sysml` + `.expected.json` (`istype`, `hastype`, `@` and `as` over `Level`, `Grade :> Real`, `Color` and `Even`, a bare scalar, a literal, a feature holding one and a cast), pilot-exec-diff `enumeration_classification.cases` (23 cases, adjudicated per case in [pilot-execution-referee.md](pilot-execution-referee.md): the twelve on a bare scalar's direct type, a literal's `Integer` supertype and value, `(3 as Level) hastype Level` and the plain `Color` agree; the eight the pilot answers otherwise — `3 istype Level` false, and `Level::high istype Level` false because it folds the literal to its Integer — contradict §8.3.7 and the pilot's own `cast-hastype-level`, and the three casts draw no output), `conformance/value_classification_at.sysml` + `.expected.json` (`n3 @ Integer`, `n3 @ Real`, `n3 @ String`, `seqInt @ Integer`, `car @ Vehicle` beside the same subjects under `hastype` and the metadata forms `belt @ Safety`, `belt @@ SysML::PartUsage`), `conformance/value_classification_shared_rule.sysml` + `.expected.json` (`istype`, `hastype`, `@` and `as` over an integer, a whole real, a quotient, a quantity, an enumeration literal, an object, a sequence, a mixed sequence, an empty value and a declared `Even`, each answering by the one rule), pilot-exec-diff `w6d:istype-*`, `:hastype-*`, `at-*`, `real-at-integer`, `seq-at-integer`, `mixed-at-*`, `mixed-istype-integer`, `empty-at-integer`, `car-at-*`, and the 27 `scalar_classification.cases` (`whole-istype-integer`, `whole-hastype-rational`, `quotient-istype-integer`, `intdiv-hastype-rational`, `rational-feature-hastype-integer`, `infinity-hastype-positive`, `empty-istype-integer`, …) | ✅ Faithful — **externally refereed**: all 20 cases agree with the pinned reference, which they did not before (they were `ours-error`; the execution report moved from 31 to 51 agreeing of 94 cases), and the ten `@` cases agree too (65 agreeing of 104): `a : Integer = 3` answers `a @ Integer` and `a @ Real` `true`, `a @ String` `false`, and `car : Car` answers `car @ Vehicle` `true`, where before `a @ Integer` was reported as classifying no element. The referee settles what the direct type is: `nat3 : Natural = 3` answers `hastype Integer` `true`, so it is the value's type and not the declaring feature's, `w : Real = 4.0` answers `w istype Integer` `false`, `w hastype Rational` `true` and `w hastype Real` `false`, so a real is judged by its representation and never by its magnitude, and `6 / 3 istype Integer` is `false`, a quotient being the `Rational` `IntegerFunctions::'/'` returns (§9.4.11.1), while `car : Car` answers `istype Vehicle` `true` and `hastype Vehicle` `false`. The one scalar case the pilot answers otherwise is `nat : Natural = 7` under `nat istype Natural` — `true` here, `false` from the pilot, which reads the literal's type alone; a feature's values are instances of all its types (§8.3.3.3.4), so the declared type counts and the verdict stays (adjudicated in [pilot-execution-referee.md](pilot-execution-referee.md)). A mixed sequence `(1, 2.5, 3)` is `false` under `istype Integer` and `true` under `@ Integer`, and an empty one `true` under `istype` and `false` under `@`, as the reference answers (`AtFunction` tests any value, `IsTypeFunction` every one; `mixed-at-integer`, `mixed-istype-integer`, `empty-at-integer`, `empty-istype-integer` of `scalar_classification.cases`). A type operand that does not resolve is `ErrUnresolvedType` and a value whose direct type cannot be determined is `ErrUndeterminedValueType` — reported, never answered `false`. One local rule the referee does not cover: a feature declared `[0..1]` with no value classifies as the empty collection on an instantiated object, and at model level by what its declared types and count settle (`cast.go` `classifyUndetermined`: `none : Integer[0..1]` answers `istype Integer` `true`, `@ Integer` ``, `@ String` `false`) | | Cast expressions evaluated at runtime (`x as T`; KerML 1.1 8.3.4.9 CastExpression, whose result is the values of `x` that `T` classifies, so it selects values and converts none — `ToInteger` and its siblings stay the library functions that convert) | `runtime/cast.go` `evalCast`, `castValue`, `castKeeps`, `declaredOperandTypes` over `runtime/classification.go` `classifyValue` — the one classification `istype`, `hastype`, `@` and a feature write (`write_conformance.go` `valueConforms`) answer from, so no two of them can judge the same value and target differently — and `semantics/cast.go` `Model.ClassifiesTypes`: the types a value is of decide the cast where they are enough, and where `T` is narrower than all of them the value's own content does (`classifyNarrower`): a scalar by its representation (`representationClassifies`, `representationPrim`: an integer is an `Integer`, a finite real a `Rational` whatever number it holds, an infinity a `Real`, a Complex a `Complex` on the real axis or off it — KerML 1.0 §8.4.4.9.2 — so nothing is judged by magnitude), a quantity by whether its unit is commensurable with the dimension `T` fixes, an object and an enumeration literal by the types they carry, a structured value (an array, a vector, a vector or tensor quantity, a measurement reference, a coordinate frame, a coordinate transformation) by the shape, units and frame reading `write_conformance.go` `valueConforms` already applies to a value written to a feature of that type — so a vector quantity of three axes is not a `ScalarQuantityValue`; `Natural` and `Positive` mark no evaluated value — no evaluation yields a value whose own type is either — so their bounds (§9.3.2.2.4, §9.3.2.2.7) refuse a negative integer, and zero for `Positive` (`positiveScalar`), and leave an in-bound integer no declaration types so undecided, exactly as `Even :> Integer` against `5` — where an enumeration target is decided by equality with its enumerated values (SysML v2 §8.3.7, the classification row above), so `3 as Level` is the value `Level::high`, held with the literal's identity (`castKept` over `Context.asEnumerated`), `2 as Level` is `()` and `(1, 2, 3, 4) as Level` is `(1, 3)`; the ScalarValues type a scalar is of is read from the library rather than resolved by name in the scope reading it (`scalarLibraryType`), so a declaration wearing one of those names changes no cast's verdict; the type a value's own feature is declared with is a type it is of as well (`declaredOperandTypes`; §8.3.3.3.4, a feature's values are instances of all its types), so a custom scalar subtype and a scalar-valued enumeration keep the values declared with them, and a written sequence is cast entry by entry (`castEntries`), each entry judged by the types its own expression is declared with and none by another's, at any nesting depth and whatever number of values an entry holds, while `Base::Anything` — which every declaration implicitly specializes — states nothing and is left out (`semantics.IsAnything`); an expression written as a value is of the evaluation type the model reads it as (`semantics/valuetype.go` `Model.ExprResultType`, so a boolean body is a `BooleanEvaluation`); and a quantity type stating a measurement reference of its own measures every value of its dimension while one narrowing lengths by something else does not (`semantics/dimension.go` `Model.FixesMeasurementReference`); *every* type an operand is declared with counts and not only the first (`semantics/operator_conformance.go` `Model.ExprResultTypes`), so a cast to a feature's second type and a cast of a selection keep their values; a type composed of others classifies as those types do (KerML 1.0 §8.3.3): the values of a union are those of any of the types it unions, of an intersection those of every type it intersects, of a difference those of the first that are none of the rest, at any nesting depth and cycle-safely (`semantics/cast.go` `Model.Classifies` over `classifiesComposed`, `semantics/model.go` `UnioningTypes`/`IntersectingTypes`/`DifferencingTypes`, applied alike by the cast, by the static cast check `Model.CastConformance` and by an object's write conformance `runtime/classify.go` `instanceConforms`, so a cast to a composed type neither warns nor is refused by the feature it is written to); a composed target weighs all the types a value is of together (`Model.ClassifiesTypes` over `instanceConforms` and `runtime/eval.go` `valueHasType`), so an object held as a type a difference subtracts is none of its values and `istype` answers so too, whether that difference is the target, a type it specializes or one an intersection of it reaches (`excludes`), while `hastype` stays on identity with one of them; the static check asks whether the two types may share a value at all (`Model.MayShareValues`, which reads a source difference as the values of the first type that are none of the rest), weighing every type the operand is declared with together, so an operand typed by both a difference's first type and one it subtracts is called unrelated, so casting a union-typed operand to one of its members is not called unrelated either, while casting to a type composed of one the operand relates to — a member of a union, a type an intersection intersects, at any nesting depth — is not called unrelated; a composed target that the types a value is of leave open is read through its operands by the classification itself (`classifyComposed`), so a bare quantity is kept by the union operand whose measurement reference its unit matches and dropped by one that fixes another, and an operand the value settles nothing about is reported as undecided only where no other operand excludes the value outright, whatever order the operands are written in; and a complex value arithmetic left on the real axis stays the `Complex` `ComplexFunctions` return, so `as Real` drops it and `re` of it is the `Real` | conformance `calc_cast_scalar_values`, `calc_cast_sequence_elementwise`, `calc_cast_enumeration`, `calc_cast_quantity`, `calc_cast_structured`, `calc_cast_instances`, `calc_cast_qualified_target`, `calc_cast_declared_types`, `calc_cast_expression_value`, `calc_cast_complex_real_axis`; `semantics/cast_classification_test.go:TestClassifiesComposedTargets`, `:TestSubtractedTypeExcludesADeclaredValue`, `:TestMayShareValuesOfComposedTypes`; `passes/typecheck_operator_test.go:TestCastConformanceRelatedTypes`, `:TestCastConformanceUnrelatedTypes`; `runtime/eval_operator_test.go:TestClassificationWeighsEveryTypeOfAnObject`; `robustness_test.go:cast_to_an_unresolved_type`, `:cast_undecided_by_the_value`, `:cast_of_a_quantity_to_a_constrained_subtype`, `:difference_typed_feature_holding_a_subtracted_object`; `passes/w8d_metadata_usage_test.go:TestW8DMetadataClassificationValuesAreModelLevelEvaluable`; conformance `value_classification_shared_rule` (the same values under `as`, `istype`, `hastype` and `@`); pilot-exec-diff `integer-as-real`, `integer-as-natural`, `fraction-as-integer`, `whole-as-integer`, `sequence-as-integer`, `car-as-vehicle`, `car-as-car`, `int-as-rational`, `real-as-real` and the `scalar_classification.cases` that fix the rule through `istype`/`hastype` | ✅ Faithful (a value `T` classifies is answered unchanged, one it does not is the empty sequence, and a sequence is filtered element-wise in order — `4.0 as Integer` and `2.5 as Integer` are `()` — a finite real is a `Rational` whatever number it holds (§8.4.4.9.2), a cast converts nothing and `RealFunctions::ToInteger` is what converts — `4 as Rational` is `4`, `(1, 2.5, 3) as Integer` is `(1, 3)`. A target no value's type and no value content settles — a user-defined specialization of a scalar or quantity type, such as `Even :> Integer` or `RoomLength :> LengthValue`, whose membership a bare `5` or `5 [m]` does not state — is `ErrUndecidedClassification`, reported rather than answered as the empty sequence; read from a feature declared with that type (`attribute e : Even = 4`, `attribute room : RoomLength = 4 [m]`) the same value is kept, the declaration being what states which of the supertype's values it is. Classifying a value is model-level evaluable, so `x = 1 as Integer` in a metadata body is accepted (`semantics/evaluable.go` `evaluableOperator` reads the named type instead of folding the operand, as it does for `istype`/`hastype`), while an unresolved target or an operand that is not evaluable there is still refused. The cast keeps exactly the values `istype` affirms, read from the same `classifyValue`: `r : Real = 4.0` answers `r istype Integer` `false` and `r as Integer` `()`, `n : Integer = 7` answers `n istype Natural` `false` and `n as Natural` the typed `ErrUndecidedClassification` — no evaluation yields a value whose own type is `Natural`, so the bound alone can refuse (`-1 as Natural` is `()`) but not affirm, as with `5 as Even` — while `nat : Natural = 7` answers `nat istype Natural` `true` and `nat as Natural` `7`, the declaration stating it. The pinned reference draws no output at all for a cast but evaluates `istype` and `hastype`, and the 26 agreeing `scalar_classification.cases` fix the scalar rule the cast borrows (`w : Real = 4.0` is no `Integer` and is a `Rational`; `6 / 3` is no `Integer`), so the cast is refereed through them. A scalar's own type is read from the library (`castTypes`, `scalarLibraryType`), so a cast decides the same way in a scope that writes `ScalarValues::Integer` out in full and imports nothing. An empty result is a result: it keeps the unit the source's elements measure in (`sequenceFrom`), so `sum((5 [m], 2 [m]) as DurationValue)` is `0 [m]`; a feature the cast may drop everything from needs multiplicity `[0..1]`, and `return : Integer = r as Integer` reports a multiplicity violation for `r = 2.5` because a lower bound of 1 is unsatisfiable by `()`) | -| Extent expressions evaluated at runtime (`all T`; KerML 1.0 §7.4.9.2 Operator Expressions — the extent operator takes a type name and "evaluates to a sequence of all instances of the named type"; §8.2.5.8.1 `ExtentExpression`, Table 5 mapping `all` to `BaseFunctions::'all'`, "Type extent", model-level evaluable "No"; §7.3.2.1 "the set of things classified by a type is the extent of the type"; §8.3.4.8.17 OperatorExpression; §9.4.2 `abstract function 'all' { return : Object[0..*]; }`). The typer gives `all T` the element type `T` and the multiplicity `[0..*]`, so `attribute xs = all T;` is a collection an `#`, `size` or a chain reads (an alias resolves to its target; a usage's extent is typed by the usage's types), and judges its value element by element as an instance of `T`, as it judges any collection, where a collection binds (`attribute flags : Boolean[*] = all Flags;`); a condition `all T` is refused for every `T` (`constraint expression must be Boolean, found the extent of Color, a sequence`), a Boolean-typed `T` included, since the extent is a sequence and never the one Boolean the run-time condition reader takes, and so is an extent as the operand of a Boolean operator (`not all Flags`, `all Flags and true`, `if all Flags ? 1 else 2`), while an operation reducing it (`size(all Flags) == 2`) is judged by its own result; and `all Car as String` warns that the cast selects nothing. The runtime materializes objects on request, so no run holds the extent the spec's Object semantics describes in the abstract; the extent an evaluation answers is the **run's**: what the run has materialized and what its context reaches, in declaration order, less what `destroy` ended — a destroyed object is released from the extent while a feature still naming it keeps that value (see the object-lifecycle row of the Instantiation map). For a **variation** usage or definition it is the variants it declares, each the object that variant stands for (the object a selection of it would bind, materialized once per owner and reused by a later selection); for an ordinary **definition or usage** it is every object of the run classified by it — the objects materialized so far, the object usages every namespace of every document of the model declares — library packages included, so `all Clock` answers `Time::universalClock` — that may hold one, in document-name then declaration order (the order `.metadata` gives cross-file annotations), and the nested usages it types reachable from those, materialized as the extent is taken; the extent is of the type, not of what the expression's namespace imports or sees, so a usage in an unrelated, un-imported package counts as one beside the expression does — only the nested usages whose type may hold an object of `T` are materialized for it, the rest contribute what they already hold, so `all Garage` leaves the garage's tools unread and a failing read of them cannot end it (an object linked into a recursive composition is walked through the links a run wrote, and each object on the way has every feature that may hold a `T` read but the one that would create another object of a declaration already on the path — settled by the value's possible types where they agree, else by what reading it makes, a read making one being undone — so a composition recursing through one declaration is not materialized under itself again while the wheel of every `Node` a link reaches is, and a value choosing at run time between another `Node` and a `Wheel` contributes the wheel; one making both at once, `(new Wheel(), new Node())`, is undone and refused with `ErrExtentUnavailable` naming it where the wheel may lead to a `T`, since it can be kept neither whole nor in part); the result is charged to the run's element budget like any collection; for an **enumeration** it is its declared literals, each with its declared value; for any other **data type** — a scalar domain (`all Integer`, `all String`) or a structured one (`attribute def Point { attribute x : Real; }`, `all Point`) — it is a typed `ErrUnboundedExtent`, not an empty sequence and not the values the run's attributes happen to hold: a data value is not created by a run (KerML 1.0 §7.4.2: data types "classify things that do not exist in time or space", distinguished only by their feature values, so a `Point` is one for every `Real` its `x` may hold), and the value a run's `origin : Point` holds is one value of the type, not its extent. A nested usage the walk cannot materialize (one exceeding the element budget, say) ends the extent with that usage's typed error, never an extent short of it. A namespace-level object usage given a value (`ref part car : Car = new Car();`, `part fleet = new Truck();`, `ref part alias : Car = spare;`) is **bound** to that value for the run: a `FeatureValue` binds its feature to its expression's result through a `BindingConnector` (KerML 1.0 §7.4.11 Feature Values, §8.3.4.10.2 `FeatureValue` — "the result of the `valueExpression` is bound to the `featureWithValue` using a `BindingConnector`", §8.4.4.11 Feature Value Semantics), so the value is read once and every read of the usage — and `all Car`, before any read of it — yields the one object it denotes; a `default` value binds nothing at its declaration but is realized for any individual of the featuring type given no other value (§8.3.4.10.2), which at namespace level is the run alone, and an initial value (`:=`) binds at the start shot, which at namespace level the run's start is, so both are held for the run the same way, while a `default` nested in a definition stays what each object built from it reads at construction. A valued usage is reached for every type its value may yield — its declared type, its value's static types, or a subtype of either (`part fleet = new Truck();` for `all Car`, not for `all Boat`; `ref part lead : Car = cars#(1);` for `all Truck`). A usage bound to an extent of its own type (`ref part cars : Car[*] = all Car;`) stands for no object while its value is being bound, and a usage whose value depends on it (`ref part lead : Car = cars#(1);`) for none yet, so the extent binds to the objects there are; a value reaching back to its own usage (`ref part loop : Car = loop;`) is refused as a cyclic feature value, and so is an extent that would read it. A binding is made whole or not at all: a value refused after constructing objects (`ref part car : Car = new Boat();`) leaves none of them nor their behaviors behind, however often the usage is read, so no extent counts them. A namespace-level object usage of several occurrences and no value (`part wheels : Wheel[2];`, `item links : Link[3];`, `part hubs : Hub[1..*];`) **denotes its lower bound of objects** for the run (§7.3.4.3 Multiplicities — a feature of multiplicity [2] has exactly two values; a `[1..*]` usage denotes one, a `[0..*]` or `[0..1]` usage holds nothing of itself and denotes none), materialized once and memoized as the `[1]` case is, through the same member materializer a collection nested in an object is filled with, so `all Wheel` reaches them in declaration order, before any read of the usage, a usage of exact count reads as the sequence (or set, as declared) of those objects, and `wheels#(1)`, `wheels.radius`, `size(wheels)` read them, while one of open count (`hubs`, `size(hubs)`, `hubs.wheel`) reads undetermined of that count, as a nested collection of open count does, since the run holds only its lower bound. A usage whose bound the model does not evaluate (`part wheels : Wheel[2..n];`, `part hubs : Hub[n];` over a valueless `n`) fixes no count, so it denotes nothing — neither one object nor its lower bound: an extent that may reach it is refused with `ErrExtentUnavailable` naming the usage and its bounds (`wheels declares [2..?] occurrences, a count the model does not fix`), and a read of it stays undetermined of the bounds the declaration does fix, as a nested collection of unknown count does; a valued one (`part wheels : Wheel[2] = (new Wheel(), new Wheel());`) is bound to its value instead, so no anonymous object is made beside the bound ones, and a value whose count violates the declared multiplicity is refused with `ErrMultiplicityViolation` naming the usage, binding nothing. The lower bound is charged to the element budget before any object is made, so `part many : Wheel[10000];` is refused with `ErrMultiplicityViolation` (the materialized-lower-bound cap) and a bound within the cap but over `OPENSYSML_MAX_ELEMENTS` with `ErrElementLimitExceeded`, each naming the usage; a member that cannot be constructed (its classifier behavior failing on entry, say) is that typed error naming the usage, and every case leaves no object, behavior or occurrence record behind, so a later read makes the collection anew or fails the same way, never an extent short of it. **Known limitation:** a package-level port (`port link : Link;`) denotes no object the runtime reaches — a port stands for an interaction point of the object holding it, and a namespace holds no object — so an extent it may contribute to (`all Link`) is refused with a typed `ErrExtentUnavailable` naming the usage rather than answered short; a port nested in a part is reached like any object, and an extent no such usage may contribute to is exact. A name resolving to nothing is `ErrUnresolvedType`; a name resolving to an element that is no type (a package, a dependency, a comment) or an operand that is no name is `ErrTypeMismatch`. `all T` is never model-level evaluable, so a `filter` or a metadata body value built on it is diagnosed rather than evaluated | `semantics/extent.go` `IsExtentExpr`, `ExtentTypeName`, `Model.ExtentOperand`, `Model.ExtentType`, `IsType`, `Model.extentTypes`, `ExtentRange`; `semantics/evaluable.go` `evaluableOperator` (`all` is never model-level evaluable); `semantics/valuetype.go` `ExprResultType`, `semantics/operator_conformance.go` `resultTypes`, `semantics/collection.go` `sourcesOf`/`valuesHeldBy`/`sourcesElements`/`judgeSources` (an extent is a collection source of its own, judged by `Model.instanceConformance` and `castTypesConformance` over its instance types, so typing it never re-enters the type it names), `semantics/valuetype.go` `operatorConformance` (`all` judged through `collectionConformance`); `runtime/extent.go` `EvalContext.evalExtent`, `literalValues`, `variantValues` (over `Context.variantValue`), `Context.objectsOf` (over `Context.heldObjectsOf` in `runtime/condition.go`, which reads only the features that may hold a target, `Context.mayHold`, and would not create an object of a declaration on the path, `Context.createdTypes`, takes what the others already hold, and returns a feature that cannot be read as the error it is) over `EvalContext.extentRoots` (the objects the run holds and the model's namespace usages that may hold one: `Context.modelUsages` walks the index's documents once per `Model` into a `usageCensus`, `Context.extentCandidates` judges it once per run and type, `namespaceObjectUsage`, `EvalContext.boundObjects`, `Context.mayHold` over `Context.givenValue`; `Context.denotedObjects` over `Context.occurrenceOf` for one occurrence and `Context.occurrencesOf` for several — `Context.namesObjects`, `Context.lowerBoundCount`, `Context.materializeMembers`, the same helpers `materializeIntrinsic` fills a nested collection with, recorded in `Context.occurrences` as the ordered objects each usage denotes; a port at namespace level that may hold one refuses the extent, `Context.undenotedUsage`), `Context.isOf`; `runtime/instance.go` `Context.denotedValue` (a read of the usage), `runtime/undetermined.go` `Context.declaredCountRefusal` (an object usage's value judged against its declared count); `runtime/eval.go` `EvalContext.declaredValue` (`Context.bindingNamespace`, `CyclicBindingError`), `Context.bindNamespace` (`Context.namespaceBindings`, one binding per run, undone with a probe, dropped with an abandoned object by `classifier_behavior.go` `abandonInstancesBetween`, carried into a re-analysis with the object by `adopt.go` `adopter.carryDerived` while everything the value read still reads as it did — each declaration's text, each name looked up by the declaration it now denotes, each type judged by its hierarchy and members, and the census of namespace usages an extent walked (`binding_reads.go` `noteCensusRead`), so a nearer declaration shadowing a name, a supertype added in another document or an object usage declared in any document reads the binding again); `runtime/errors.go` `ErrUnboundedExtent`, `ErrExtentUnavailable` | `semantics/extent_test.go:TestExtentExpressionTypes`, `:TestExtentExpressionConformance`; `semantics/evaluable_test.go:TestModelLevelEvaluable`; `passes/typecheck_expr_test.go:TestExprExtentConditionMustBeBoolean` (conditions, guards and Boolean operators alike), `passes/typecheck_operator_test.go:TestCastConformanceExtent`; `passes/filter_test.go:TestFilterExtentIsNotModelLevelEvaluable`, `passes/w8c_metadata_annotation_test.go:TestMetadataBodyValueRejectsAnExtent`; conformance `extent_variation_variants.sysml`, `extent_definition_objects.sysml`, `extent_enumeration_values.sysml`, `extent_unbounded_data_type.sysml` (`all Integer`, `all String`, `all Point` beside an `origin : Point` the run holds), `extent_declaration_order.sysml` + `.trace.golden` (an enumeration's values and a variation's variants in declaration order, whatever order names them elsewhere), `extent_namespace_objects.sysml` + `.trace.golden` (package-level usages materialized as the extent is taken, roots then nested objects in declaration order, another package's usage among them), `extent_enclosing_namespaces.sysml` (usages of every enclosing package and of a sibling package reached before any run read them), `extent_imported_and_unimported_packages.sysml` (a usage in an imported package and one in an un-imported package both count), `extent_library_clock.sysml` (`all Clock` and `all Item` answer `Time::universalClock`; the library's `[0..*]` collections neither count nor refuse), `extent_across_documents.sysml` + `.depot.sysml` + `.trace.golden` (a usage of another document reached, and first, its document name sorting before the expression's), `extent_linked_objects.sysml` (objects of one declaration linked by a run's assignments walked along the links), `extent_linked_repeated_declaration.sysml` (a wheel under every one of the linked objects reached, in depth-first feature declaration order), `extent_value_chosen_at_run_time.sysml` (a value choosing a wheel over another `Fork` at run time contributes it; one choosing the `Fork` is left unread), robustness `extent_through_a_value_recursing_and_not` (a value making a wheel and another `Fork` at once is refused for `all Wheel`, undone, and left unread for `all Seat`), `extent_bound_namespace_objects.sysml` + `.trace.golden` (valued usages bound once: `car === car`, `all Car` reaching them before any read), `extent_namespace_collection.sysml` + `.trace.golden` (`[2]`, `[3]`, `[1..*]` and `[0..*]` usages: `all Wheel` counting the two beside a `[1]` usage, the usage read directly, indexed, chained through and sized, members created in declaration order once and identical on every read), `extent_bound_namespace_collection.sysml` + `.trace.golden` (a valued `[2]` usage bound to its two objects, no third made; a `ref part` bound to its members denoting those), `extent_bound_namespace_collection_count.sysml` (a `[2]` usage whose value yields three refused with `ErrMultiplicityViolation`); `adopt_test.go:TestAdoptRebindsAnExtentWhenItsNamespaceChanges` (a usage added to the extent's own document or to another rebinds it); `extent_test.go:TestNamespaceBindingDenotesOneObject` (`=`, an untyped value, an alias, `default` and `:=` at namespace level each read twice as one object, a nested `default` read at construction), `:TestNamespaceBindingProbeIsUndone`, `:TestNamespaceBindingFailureLeavesNoBinding`, `:TestNamespaceBindingFailureLeavesNoObject`, `:TestNamespaceBindingToAnExtent`, `adopt_test.go:TestAdoptCarriesANamespaceBinding`; `element_budget_test.go:TestElementBudgetBoundsExtents`; `robustness_test.go:extent_of_an_unresolved_or_unbounded_type` (an unresolved name, a scalar and a structured data type, a package, a dependency, a comment), `extent_reaching_a_namespace_collection` (a `[2]` usage's two objects counted beside a nested one, the usage read as those two objects and a `[1..*]` usage read undetermined, a `[0..*]` usage contributing none, the `ErrExtentUnavailable` refusal for a port at namespace level, and the same objects on a second evaluation), `namespace_collection_that_cannot_be_constructed` (a `[2]` usage whose members' classifier behavior fails on entry is that typed error naming the usage, leaving no object, behavior or occurrence record, however often it is read), `namespace_collection_over_budget` (`[10000]` refused by the lower-bound cap, `[5]` over the element budget, each leaving nothing behind), `extent_over_an_object_that_cannot_be_read` (a nested usage over the element budget ends the extent with `ErrElementLimitExceeded`, and is not read for the extent of a type it cannot hold), `extent_reaching_a_far_usage_that_cannot_be_read` (a usage of another document whose value its type refuses ends the extent with `ErrTypeMismatch` naming it and is not read for a type it cannot hold, while a `[2]` collection of another document is counted), `extent_over_far_usages_under_the_element_budget` (usages of two other documents materialized in document order, the second ending the extent with `ErrElementLimitExceeded` naming it and its collection; a budget covering both answers), `extent_over_recursive_composition` (a part of its own type, a constructor of it and two types holding each other each end, every object created having its own wheel read); `cmd/sysml/run_trade_study_corpus_test.go:TestRunTradeStudyPilotTradeOff`; compiled: `repl/compile_test.go` `Refused::Extent` | ⚠️ Approximate — the spec's extent is every instance of the type; this evaluator answers the instances the run holds or reaches — every namespace-level object usage of the loaded model, the objects those hold, and what the run made — so a usage nested in a definition nothing instantiates (`part def Garage { part car : Car; }` with no `Garage` usage or object) contributes no `Car`, a document not loaded into the index contributes nothing, two runs materializing different objects answer different extents of the same definition, and the order is document-name then declaration order, which the spec leaves to the `sequence` (adjudicated in [pilot-execution-referee.md](pilot-execution-referee.md)). **Behavior change:** the extent used to reach only the usages of the namespaces lexically enclosing the expression, so `size(all Car)` in a package importing `Gaps` under-counted `Gaps::car`; it now reaches the whole model. **Behavior change:** a namespace-level valued object usage used to be evaluated anew on every read, so `car` read twice was two `Car`s; it now denotes one object for the run, as the binding the spec makes of its value. The variation, enumeration and unbounded cases are the finite extents the model itself fixes and are ✅ faithful. **Behavior change:** a namespace-level usage of several occurrences used to denote no object, so an extent it might contribute to was refused with `ErrExtentUnavailable` and a chain through it was undetermined; it now denotes its lower bound of objects, as a collection nested in an object does. An extent a package-level port may contribute to is still refused, not answered, until the runtime denotes what such a port stands for. The library types the result `Object[0..*]`; an enumeration's literals are answered as its extent because they are the only instances it has. Native code generation refuses `all` with a typed `operator 'all'` refusal ([native-compilation.md](native-compilation.md)) | +| Extent expressions evaluated at runtime (`all T`; KerML 1.0 §7.4.9.2 Operator Expressions — the extent operator takes a type name and "evaluates to a sequence of all instances of the named type"; §8.2.5.8.1 `ExtentExpression`, Table 5 mapping `all` to `BaseFunctions::'all'`, "Type extent", model-level evaluable "No"; §7.3.2.1 "the set of things classified by a type is the extent of the type"; §8.3.4.8.17 OperatorExpression; §9.4.2 `abstract function 'all' { return : Object[0..*]; }`). The typer gives `all T` the element type `T` and the multiplicity `[0..*]`, so `attribute xs = all T;` is a collection an `#`, `size` or a chain reads (an alias resolves to its target; a usage's extent is typed by the usage's types), and judges its value element by element as an instance of `T`, as it judges any collection, where a collection binds (`attribute flags : Boolean[*] = all Flags;`); a condition `all T` is refused for every `T` (`constraint expression must be Boolean, found the extent of Color, a sequence`), a Boolean-typed `T` included, since the extent is a sequence and never the one Boolean the run-time condition reader takes, and so is an extent as the operand of a Boolean operator (`not all Flags`, `all Flags and true`, `if all Flags ? 1 else 2`), while an operation reducing it (`size(all Flags) == 2`) is judged by its own result; and `all Car as String` warns that the cast selects nothing. The runtime materializes objects on request, so no run holds the extent the spec's Object semantics describes in the abstract; the extent an evaluation answers is the **run's**: what the run has materialized and what its context reaches, in declaration order, less what `destroy` ended — a destroyed object is released from the extent while a feature still naming it keeps that value (see the object-lifecycle row of the Instantiation map). For a **variation** usage or definition it is the variants it declares, each the object that variant stands for (the object a selection of it would bind, materialized once per owner and reused by a later selection); for an ordinary **definition or usage** it is every object of the run classified by it — the objects materialized so far, the object usages every namespace of every document of the model declares — library packages included, so `all Clock` answers `Time::universalClock` — that may hold one, in document-name then declaration order (the order `.metadata` gives cross-file annotations), and the nested usages it types reachable from those, materialized as the extent is taken; the extent is of the type, not of what the expression's namespace imports or sees, so a usage in an unrelated, un-imported package counts as one beside the expression does — only the nested usages whose type may hold an object of `T` are materialized for it, the rest contribute what they already hold, so `all Garage` leaves the garage's tools unread and a failing read of them cannot end it (an object linked into a recursive composition is walked through the links a run wrote, and each object on the way has every feature that may hold a `T` read but the one that would create another object of a declaration already on the path — settled by the value's possible types where they agree, else by what reading it makes, a read making one being undone — so a composition recursing through one declaration is not materialized under itself again while the wheel of every `Node` a link reaches is, and a value choosing at run time between another `Node` and a `Wheel` contributes the wheel; one making both at once, `(new Wheel(), new Node())`, is undone and refused with `ErrExtentUnavailable` naming it where the wheel may lead to a `T`, since it can be kept neither whole nor in part); the result is charged to the run's element budget like any collection; for an **enumeration** it is its declared literals, each with its declared value; for any other **data type** — a scalar domain (`all Integer`, `all String`) or a structured one (`attribute def Point { attribute x : Real; }`, `all Point`) — it is a typed `ErrUnboundedExtent`, not an empty sequence and not the values the run's attributes happen to hold: a data value is not created by a run (KerML 1.0 §7.4.2: data types "classify things that do not exist in time or space", distinguished only by their feature values, so a `Point` is one for every `Real` its `x` may hold), and the value a run's `origin : Point` holds is one value of the type, not its extent. A nested usage the walk cannot materialize (one exceeding the element budget, say) ends the extent with that usage's typed error, never an extent short of it. A namespace-level object usage given a value (`ref part car : Car = new Car();`, `part fleet = new Truck();`, `ref part alias : Car = spare;`) is **bound** to that value for the run: a `FeatureValue` binds its feature to its expression's result through a `BindingConnector` (KerML 1.0 §7.4.11 Feature Values, §8.3.4.10.2 `FeatureValue` — "the result of the `valueExpression` is bound to the `featureWithValue` using a `BindingConnector`", §8.4.4.11 Feature Value Semantics), so the value is read once and every read of the usage — and `all Car`, before any read of it — yields the one object it denotes; a `default` value binds nothing at its declaration but is realized for any individual of the featuring type given no other value (§8.3.4.10.2), which at namespace level is the run alone, and an initial value (`:=`) binds at the start shot, which at namespace level the run's start is, so both are held for the run the same way, while a `default` nested in a definition stays what each object built from it reads at construction. A valued usage is reached for every type its value may yield — its declared type, its value's static types, or a subtype of either (`part fleet = new Truck();` for `all Car`, not for `all Boat`; `ref part lead : Car = cars#(1);` for `all Truck`). A usage bound to an extent of its own type (`ref part cars : Car[*] = all Car;`) stands for no object while its value is being bound, and a usage whose value depends on it (`ref part lead : Car = cars#(1);`) for none yet, so the extent binds to the objects there are; a value reaching back to its own usage (`ref part loop : Car = loop;`) is refused as a cyclic feature value, and so is an extent that would read it. A binding is made whole or not at all: a value refused after constructing objects (`ref part car : Car = new Boat();`) leaves none of them nor their behaviors behind, however often the usage is read, so no extent counts them. A namespace-level object usage of several occurrences and no value (`part wheels : Wheel[2];`, `item links : Link[3];`, `part hubs : Hub[1..*];`) **denotes its lower bound of objects** for the run (§7.3.4.3 Multiplicities — a feature of multiplicity [2] has exactly two values; a `[1..*]` usage denotes one, a `[0..*]` or `[0..1]` usage holds nothing of itself and denotes none), materialized once and memoized as the `[1]` case is, through the same member materializer a collection nested in an object is filled with, so `all Wheel` reaches them in declaration order, before any read of the usage, a usage of exact count reads as the sequence (or set, as declared) of those objects, and `wheels#(1)`, `wheels.radius`, `size(wheels)` read them, while one of open count (`hubs`, `size(hubs)`, `hubs.wheel`) reads undetermined of that count, as a nested collection of open count does, since the run holds only its lower bound. A usage whose bound the model does not evaluate (`part wheels : Wheel[2..n];`, `part hubs : Hub[n];` over a valueless `n`) fixes no count, so it denotes nothing — neither one object nor its lower bound: an extent that may reach it is refused with `ErrExtentUnavailable` naming the usage and its bounds (`wheels declares [2..?] occurrences, a count the model does not fix`), and a read of it stays undetermined of the bounds the declaration does fix, as a nested collection of unknown count does; a valued one (`part wheels : Wheel[2] = (new Wheel(), new Wheel());`) is bound to its value instead, so no anonymous object is made beside the bound ones, and a value whose count violates the declared multiplicity is refused with `ErrMultiplicityViolation` naming the usage, binding nothing. The lower bound is charged to the element budget before any object is made, so `part many : Wheel[10000];` is refused with `ErrMultiplicityViolation` (the materialized-lower-bound cap) and a bound within the cap but over `OPENSYSML_MAX_ELEMENTS` with `ErrElementLimitExceeded`, each naming the usage; a member that cannot be constructed (its classifier behavior failing on entry, say) is that typed error naming the usage, and every case leaves no object, behavior or occurrence record behind, so a later read makes the collection anew or fails the same way, never an extent short of it. **Known limitation:** a package-level port (`port link : Link;`) denotes no object the runtime reaches — a port stands for an interaction point of the object holding it, and a namespace holds no object — so an extent it may contribute to (`all Link`) is refused with a typed `ErrExtentUnavailable` naming the usage rather than answered short; a port nested in a part is reached like any object, and an extent no such usage may contribute to is exact. A name resolving to nothing is `ErrUnresolvedType`; a name resolving to an element that is no type (a package, a dependency, a comment) or an operand that is no name is `ErrTypeMismatch`. `all T` is never model-level evaluable, so a `filter` or a metadata body value built on it is diagnosed rather than evaluated | `semantics/extent.go` `IsExtentExpr`, `ExtentTypeName`, `Model.ExtentOperand`, `Model.ExtentType`, `IsType`, `Model.extentTypes`, `ExtentRange`; `semantics/evaluable.go` `evaluableOperator` (`all` is never model-level evaluable); `semantics/valuetype.go` `ExprResultType`, `semantics/operator_conformance.go` `resultTypes`, `semantics/collection.go` `sourcesOf`/`valuesHeldBy`/`sourcesElements`/`judgeSources` (an extent is a collection source of its own, judged by `Model.instanceConformance` and `castTypesConformance` over its instance types, so typing it never re-enters the type it names), `semantics/valuetype.go` `operatorConformance` (`all` judged through `collectionConformance`); `runtime/extent.go` `EvalContext.evalExtent`, `literalValues`, `variantValues` (over `Context.variantValue`), `Context.objectsOf` (over `Context.heldObjectsOf` in `runtime/condition.go`, which reads only the features that may hold a target, `Context.mayHold`, and would not create an object of a declaration on the path, `Context.createdTypes`, takes what the others already hold, and returns a feature that cannot be read as the error it is) over `EvalContext.extentRoots` (the objects the run holds and the model's namespace usages that may hold one: `Context.modelUsages` walks the index's documents once per `Model` into a `usageCensus`, `Context.extentCandidates` judges it once per run and type, `namespaceObjectUsage`, `EvalContext.boundObjects`, `Context.mayHold` over `Context.givenValue`; `Context.denotedObjects` over `Context.occurrenceOf` for one occurrence and `Context.occurrencesOf` for several — `Context.namesObjects`, `Context.lowerBoundCount`, `Context.materializeMembers`, the same helpers `materializeIntrinsic` fills a nested collection with, recorded in `Context.occurrences` as the ordered objects each usage denotes; a port at namespace level that may hold one refuses the extent, `Context.undenotedUsage`), `Context.isOf`; `runtime/instance.go` `Context.denotedValue` (a read of the usage), `runtime/undetermined.go` `Context.declaredCountRefusal` (an object usage's value judged against its declared count); `runtime/eval.go` `EvalContext.declaredValue` (`Context.bindingNamespace`, `CyclicBindingError`), `Context.bindNamespace` (`Context.namespaceBindings`, one binding per run, undone with a probe, dropped with an abandoned object by `classifier_behavior.go` `abandonInstancesBetween`, carried into a re-analysis with the object by `adopt.go` `adopter.carryDerived` while everything the value read still reads as it did — each declaration's text, each name looked up by the declaration it now denotes, each type judged by its hierarchy and members, and the census of namespace usages an extent walked (`binding_reads.go` `noteCensusRead`), so a nearer declaration shadowing a name, a supertype added in another document or an object usage declared in any document reads the binding again); `runtime/errors.go` `ErrUnboundedExtent`, `ErrExtentUnavailable` | `semantics/extent_test.go:TestExtentExpressionTypes`, `:TestExtentExpressionConformance`; `semantics/evaluable_test.go:TestModelLevelEvaluable`; `passes/typecheck_expr_test.go:TestExprExtentConditionMustBeBoolean` (conditions, guards and Boolean operators alike), `passes/typecheck_operator_test.go:TestCastConformanceExtent`; `passes/filter_test.go:TestFilterExtentIsNotModelLevelEvaluable`, `passes/w8c_metadata_annotation_test.go:TestMetadataBodyValueRejectsAnExtent`; conformance `extent_variation_variants.sysml`, `extent_definition_objects.sysml`, `extent_enumeration_values.sysml`, `extent_unbounded_data_type.sysml` (`all Integer`, `all String`, `all Point` beside an `origin : Point` the run holds), `extent_declaration_order.sysml` + `.trace.golden` (an enumeration's values and a variation's variants in declaration order, whatever order names them elsewhere), `extent_namespace_objects.sysml` + `.trace.golden` (package-level usages materialized as the extent is taken, roots then nested objects in declaration order, another package's usage among them), `extent_enclosing_namespaces.sysml` (usages of every enclosing package and of a sibling package reached before any run read them), `extent_imported_and_unimported_packages.sysml` (a usage in an imported package and one in an un-imported package both count), `extent_library_clock.sysml` (`all Clock` and `all Item` answer `Time::universalClock`; the library's `[0..*]` collections neither count nor refuse), `extent_across_documents.sysml` + `.depot.sysml` + `.trace.golden` (a usage of another document reached, and first, its document name sorting before the expression's), `extent_linked_objects.sysml` (objects of one declaration linked by a run's assignments walked along the links), `extent_linked_repeated_declaration.sysml` (a wheel under every one of the linked objects reached, in depth-first feature declaration order), `extent_value_chosen_at_run_time.sysml` (a value choosing a wheel over another `Fork` at run time contributes it; one choosing the `Fork` is left unread), robustness `extent_through_a_value_recursing_and_not` (a value making a wheel and another `Fork` at once is refused for `all Wheel`, undone, and left unread for `all Seat`), `extent_bound_namespace_objects.sysml` + `.trace.golden` (valued usages bound once: `car === car`, `all Car` reaching them before any read), `extent_namespace_collection.sysml` + `.trace.golden` (`[2]`, `[3]`, `[1..*]` and `[0..*]` usages: `all Wheel` counting the two beside a `[1]` usage, the usage read directly, indexed, chained through and sized, members created in declaration order once and identical on every read), `extent_bound_namespace_collection.sysml` + `.trace.golden` (a valued `[2]` usage bound to its two objects, no third made; a `ref part` bound to its members denoting those), `extent_bound_namespace_collection_count.sysml` (a `[2]` usage whose value yields three refused with `ErrMultiplicityViolation`); `adopt_test.go:TestAdoptRebindsAnExtentWhenItsNamespaceChanges` (a usage added to the extent's own document or to another rebinds it); `extent_test.go:TestNamespaceBindingDenotesOneObject` (`=`, an untyped value, an alias, `default` and `:=` at namespace level each read twice as one object, a nested `default` read at construction), `:TestNamespaceBindingProbeIsUndone`, `:TestNamespaceBindingFailureLeavesNoBinding`, `:TestNamespaceBindingFailureLeavesNoObject`, `:TestNamespaceBindingToAnExtent`, `adopt_test.go:TestAdoptCarriesANamespaceBinding`; `element_budget_test.go:TestElementBudgetBoundsExtents`; `robustness_test.go:extent_of_an_unresolved_or_unbounded_type` (an unresolved name, a scalar and a structured data type, a package, a dependency, a comment), `extent_reaching_a_namespace_collection` (a `[2]` usage's two objects counted beside a nested one, the usage read as those two objects and a `[1..*]` usage read undetermined, a `[0..*]` usage contributing none, the `ErrExtentUnavailable` refusal for a port at namespace level, and the same objects on a second evaluation), `namespace_collection_that_cannot_be_constructed` (a `[2]` usage whose members' classifier behavior fails on entry is that typed error naming the usage, leaving no object, behavior or occurrence record, however often it is read), `namespace_collection_over_budget` (`[10000]` refused by the lower-bound cap, `[5]` over the element budget, each leaving nothing behind), `extent_over_an_object_that_cannot_be_read` (a nested usage over the element budget ends the extent with `ErrElementLimitExceeded`, and is not read for the extent of a type it cannot hold), `extent_reaching_a_far_usage_that_cannot_be_read` (a usage of another document whose value its type refuses ends the extent with `ErrTypeMismatch` naming it and is not read for a type it cannot hold, while a `[2]` collection of another document is counted), `extent_over_far_usages_under_the_element_budget` (usages of two other documents materialized in document order, the second ending the extent with `ErrElementLimitExceeded` naming it and its collection; a budget covering both answers), `extent_over_recursive_composition` (a part of its own type, a constructor of it and two types holding each other each end, every object created having its own wheel read); `extent_namespace_binding` + `.trace.golden` (a package-owned `bind` joining two valueless usages into one object and one to a feature chain), `extent_held_performances` + `.trace.golden` (`all` reaching an object's action, state, connection, interface and allocation usages, perform and exhibit included, while constraint, requirement and calc usages stay out), `extent_namespace_subsetters` + `.trace.golden` (a namespace collection usage honoring the usages subsetting it, an abstract usage holding their values alone, a `[1]` its subsetter's object); `robustness_extent_model_determined_test.go:TestRuntimeRobustnessExtentModelDetermined`; `parse/binding_namespace_level.golden`; `cmd/sysml/run_trade_study_corpus_test.go:TestRunTradeStudyPilotTradeOff`; compiled: `repl/compile_test.go` `Refused::Extent` | ⚠️ Approximate — the spec's extent is every instance of the type; this evaluator answers the instances the run holds or reaches — every namespace-level object usage of the loaded model, the objects those hold, and what the run made — so a usage nested in a definition nothing instantiates (`part def Garage { part car : Car; }` with no `Garage` usage or object) contributes no `Car`, a document not loaded into the index contributes nothing, two runs materializing different objects answer different extents of the same definition, and the order is document-name then declaration order, which the spec leaves to the `sequence` (adjudicated in [pilot-execution-referee.md](pilot-execution-referee.md)). **Behavior change:** the extent used to reach only the usages of the namespaces lexically enclosing the expression, so `size(all Car)` in a package importing `Gaps` under-counted `Gaps::car`; it now reaches the whole model. **Behavior change:** a namespace-level valued object usage used to be evaluated anew on every read, so `car` read twice was two `Car`s; it now denotes one object for the run, as the binding the spec makes of its value. The variation, enumeration and unbounded cases are the finite extents the model itself fixes and are ✅ faithful. **Behavior change:** a namespace-level usage of several occurrences used to denote no object, so an extent it might contribute to was refused with `ErrExtentUnavailable` and a chain through it was undetermined; it now denotes its lower bound of objects, as a collection nested in an object does. An extent a package-level port may contribute to is still refused, not answered, until the runtime denotes what such a port stands for. The library types the result `Object[0..*]`; an enumeration's literals are answered as its extent because they are the only instances it has. Native code generation refuses `all` with a typed `operator 'all'` refusal ([native-compilation.md](native-compilation.md)). **Behavior change:** a binding connector owned by a namespace used to be ignored (`lower.ToBindings` reads a declaration's members, which a package body is not), so `bind a = b` left `a` and `b` denoting two objects; it now makes its ends denote the same value(s) as KerML §7.4.6.3 requires — every usage the namespace-owned bindings join is one equivalence class (union-find over a once-per-model walk of every document's namespace scopes, so a binding written in another package — `bind P::x = y;` — and several bindings on one usage both count), and the class's value is the one a valued member declares or a chain end evaluates to — several of them pairwise equal, else `ErrBindingConflict` naming the two ends that differ — or one object materialized for the earliest-declared member and classified by every member's types (`classifyHeld`), recorded for each member so whichever is read first the result and the recorded state are identical; a chain end reading a member of its own class is `CyclicBindingError`, and an end or connector multiplicity other than `1..1` or absent the typed `ErrBindingEnd` semantics nested bindings use, judged per binding (`ir/lower/binding.go` `NamespaceBindings`; `runtime/namespace_binding.go` `namespaceModelIndex`, `resolveNamespaceClass`, `namespaceBindingCounts`). **Behavior change:** `all T` used to descend only the part, item, occurrence, individual and port features an object holds; it now also reads and collects the values of its action, state, connection, interface, allocation and flow usages — perform and exhibit included — so a performance an object may hold is in the extent (`runtime/extent.go` `extentHeldFeature` admitted by the walk alone — `objectFeature`'s other callers are unchanged — `extentHeldMember` keeping a performance member of a library type from making its holder a candidate, `extentHoldsObjects`, `mayHold` seeing through the same kinds); constraint, requirement, calc and case usages stay out — their read is an evaluation, not a held occurrence — and SysML v2 §7.6.3 defaults those performance usages to `[0..*]`, so the one object such a read materializes is the run's reading, the extent agreeing with what reads produce. **Behavior change:** a namespace-level collection usage used to ignore the namespace usages subsetting it, materializing its lower bound of fresh objects; it now denotes its subsetters' objects first and anonymous members only to make up the lower bound — `part vs : Car[2]` with `part c1 : Car[1] :> vs` denotes `c1`'s object plus one anonymous, an abstract usage contributes no anonymous members of its own, an under-count below its lower bound or a count over its upper a typed `ErrMultiplicityViolation` naming it (`runtime/namespace_binding.go` `namespacedSubsetObjects`, `namespaceSubsetters`, `denotedSubsetObjects`, reusing the nested `instance.go` `materializeCompositeCollection`/`subsettingContributions` logic; `instance.go` `denotedValue` no longer reads an abstract collection as undetermined). SysML v2 §7.6.3 gives package-level usages default multiplicity `[0..*]`, so the one object a valueless package-level usage denotes, a valueless `ref` usage's object, and the lower-bound members of an open multiplicity are the run's materialization, not instances the model determines; KerML §9.4.2 declares `'all'` with `return : Object[0..*]`, not `ordered`, and §7.4.9.2 calls the result a sequence without giving an order, so the order is tool-defined (kept deterministic as above); classification stays minimal — an object is an instance only of the types the model gives it; and portions (KerML §9.2.4 `timeSlices`/`portions`) are not enumerated | | Meta-cast expressions evaluated at runtime (`x meta T`; KerML 1.0 §7.4.9.2 MetaCastExpression, the shorthand for `x.metadata as T`, so the result is the metadata of the *element* `x` names — not of its values — that `T` classifies: its metadata annotations whose type conforms to `T`, in model order, then its reflective metaobject when the element's own metaclass conforms to `T` (§8.3.4.8.15, §8.4.4.9.7); the empty sequence when neither does, so `seatBelt meta SysML::PartDefinition` is `()` for a part usage). The metaobject is a value of its own kind (`ValMetaobject`): the element together with the metaclass that classifies it — `KerML.kerml`'s and `SysML.kerml`'s reflective metaclasses — not a runtime object of that metaclass, so no derived feature is fabricated. Its identity is the element's: `(x meta KerML::Type) === (x meta KerML::Feature)`, `==` likewise, and two evaluations answer one metaobject. A feature of the metaclass read off it (`.declaredName`, `.qualifiedName`, `.ownedFeature`, `.ownedMember`, `.type`, `.direction`, `.isAbstract`, and every side-table property an element filter classifies by — `name`, `shortName`, `documentation`, `isComposite`, `isDerived`, `isEnd`, `isOrdered`, `isUnique`, `isVariable`, `isConstant`, `isPortion`, `isSufficient`, …) is derived from the element's declaration through the ordinary member-access path, shaped by the feature's declared multiplicity (one value under `[0..1]`/`[1]`, a sequence otherwise), an element-valued one answering metaobjects (`Element::documentation` is `Documentation[0..*]`, so each `doc` comment is a metaobject whose own `Comment::body` and `Comment::locale` are the strings, the locale unquoted; `qualifiedName` of an unnamed element such as a `doc` comment is `()`; `Dependency::client` and `Dependency::supplier` are the `from` and `to` elements in declaration order, and `TextualRepresentation::representedElement` — declared `subsets owner` — is the owning element, with `language` and `body` its strings) and `direction` a `FeatureDirectionKind` literal; a metaobject cast to a supertype is read by that supertype's names even where a SysML metaclass redefines them (`definition` for a usage's `type`). A feature the metaclass declares but the runtime does not derive (`ownedRelationship`, …) is `ErrReflectiveFeatureUnsupported` naming the feature and the element, as is `Comment::body` of a model never given its notation (`SetSourceText`) rather than an empty string; a name the metaclass does not declare is the ordinary missing-member error; a subject that is a value rather than an element (`1 meta KerML::Feature`) is `semantics.ErrFilterUnevaluable`; an unresolved `x` or `T` is `ErrUnresolvedReference`. Metaobjects cross `===`/`==`, set membership and canonical set order, trace and `FormatTraceValue` (`meta(test::seatBelt : SysML::Systems::PartUsage)`), carry and adoption (both symbols rebound in the adopting model) and the gRPC boundary (the `metaobject_values` row under *gRPC boundary*); native compilation refuses them (`docs/project/native-compilation.md`). `@@`, `@` and the static `SemanticMetadata::baseType` reading of a `meta` cast are unchanged | `runtime/metaobject.go` `evalMetaCast`, `metaCastSubject`, `reflectiveMetaobject`, `metaobjectFeature`, `reflectiveResult`, `metaclassFeature`, `reflectivePropertyNames`; `runtime/value.go` `ValMetaobject`, `NewMetaobject`, `MetaobjectText`; `runtime/eval.go` (`OpMeta` dispatch, `elementDenotedBy`, `chainMemberValue`); `runtime/value_equality.go`, `set_order.go`, `describe.go`, `trace.go`, `adopt.go`, `carry.go`, `classification.go`; `semantics/annotations.go` `MetaclassOf`, `ReflectiveElements`, `ReflectiveDirection`, `ReflectiveFeatureValues`, `reflectiveCommentBody`; `semantics/documentation.go` `commentBody`; `semantics/members.go` `MembersOfIncludingRedefined`, `LookupMember` | conformance `meta_cast` (annotation then metaobject, `()` for a non-instance, every listed feature, identity across casts, `documentation` metaobjects with `.body` — `""` for a blank `doc /**/`, since `Comment::body` is `String[1..1]` — `.owner`, `.qualifiedName` and `.locale`; a `dependency` and a `rep` read through `.metadata` and `meta KerML::Element`, with `.client`, `.supplier`, `.language`, `.body` and `.representedElement`), `meta_cast_trace` + `meta_cast_trace.trace.golden`; `value_kinds_test.go` (every exhaustive dispatch holds a metaobject sample; a set orders metaobjects by element, as equality does); `robustness_test.go:meta_cast_failure_modes`; `repl/compile_test.go` (`MetaCast` refused); `tools/referee/exec` `metadata_access.cases` refereed against the pilot (`docs/project/pilot-execution-referee.md`) | ✅ Faithful | | An operator with no runtime evaluation (bitwise complement, and the one the runtime evaluates from its own expression node) reports why | `eval.go` `unimplementedOperators` (`ErrUnsupportedOperator`) | `eval_operator_test.go:TestUnimplementedOperatorReportsWhy` | ⛔ Deliberate divergence (bitwise complement: KerML 1.0 §8.2.5.8.1 Table 5 marks `~` "Undefined" and not model-level evaluable — `DataFunctions::'~'` is abstract and no library the runtime applies specializes it, the pinned pilot leaves `OperatorExpression ~` unevaluated, and answering `-6` for `~5` would invent a two's-complement semantics nothing states; the runtime keeps the typed refusal and the checker warns on every use (`undefined-operator`, row below) — adjudicated in [bitwise-complement.md](bitwise-complement.md). `OpIndex` is evaluated from an IndexExpression rather than as an operator, so reaching it as one says that. Classification `istype`/`hastype`, metadata `@`/`@@`, the `as` cast, the `meta` cast and the `all` extent **are** evaluated — see the rows around this one) | | Metadata classification evaluated at runtime (`p @ Safety`, `p @@ SysML::PartUsage`, in a constraint, a calc body or an `%eval`; SysML v2 7.9.4 metadata, 8.4.4.2 ClassificationExpression) | `eval.go` `EvalContext.evalClassification` — reached by `@@`, by `@` with no subject, and by `x @ T` when T is a metadata definition or metaclass (`semantics.IsMetadataType`), since only an annotation has such a type; `x @ T` with an ordinary type is the value test of the row above — over `semantics/filter.go` `Model.EvalClassification` (the same compiled predicate and `Model.AllSupertypes` conformance an element filter is decided by, so the two paths cannot disagree); `eval.go` `classifiedElement` settles the element the subject denotes — the object being evaluated for an implicit subject or `self`, the element a name names, or an object's classifier, a selected variant or an enumeration literal | `runtime/eval_classification_test.go` (`TestEvalClassificationOverNamedSubjects`, `TestEvalClassificationMetaVersusAnnotation`, `TestEvalClassificationInACalcBody`, `TestEvalClassificationOfTheObjectBeingEvaluated`, `TestEvalClassificationAgreesWithAnElementFilter` — the verdicts of the two paths pinned equal); `conformance/filter_classification_annotation_forms.sysml`, `filter_classification_meta_versus_annotation.sysml`, `filter_classification_implicit_subject.sysml`, `view_exposed_element_classification.sysml`; `robustness_test.go` `classification_outside_the_evaluable_subset`; `semantics/classification_test.go` (the shared predicate itself) | ✅ Faithful (`@T` holds for metadata annotating the subject in every form the parser accepts, inherited conformance included, and for the subject's own metaclass; `@@T` holds for the metaclass alone. A subject that denotes no element, or a metadata type that does not resolve, is `semantics.ErrFilterUnevaluable` — reported, never answered false) | @@ -289,7 +289,7 @@ question, the boundary stated under [Objective optimization](#objective-optimiza | `subject s : T;` is an `in` parameter of the case: a usage binding it (`subject s = ship;`) supplies the object, the object a run is asked on supplies it otherwise, and a case run with neither is refused naming the subject — never run with an empty one | `runtime/analysis_run.go` `subjectDeclaration`, `Context.subjectParameter`, `calcShape.subjectParameter`, `calcShape.analysisArgs`, `calcShape.unboundSubject` | `analysis_subject_bound_in_usage.sysml`, `analysis_subject_at_run_time.sysml`, `analysis_robustness_test.go:unbound_subject`, `:subject_of_the_wrong_type`, `repl/analysis_test.go:TestAnalysisDefinitionOnAnObject` | ✅ Faithful | | A nested case binding no subject of its own runs on the enclosing case's subject (§7.22 `AnalysisCase`, the pilot's `10a-Analysis.sysml` `massAnalysisCase` comment) | `runtime/analysis_run.go` `Context.enclosingSubject`, `enclosingBehavior` | `analysis_nested_default_subject.sysml` | ✅ Faithful | | `in` parameters are bound positionally or by name from the arguments, or from their declared defaults; the subject is never a positional parameter; a missing value, a surplus argument and an unknown name are typed refusals | `runtime/analysis_run.go` `calcShape.analysisArgs`; `runtime/invoke_calc.go` `bindCalcParameter` | `analysis_in_parameter_positional.sysml`, `analysis_in_parameter_named.sysml`, `analysis_robustness_test.go:missing_in_parameter`, `:too_many_arguments`, `:unknown_named_argument` | ✅ Faithful | -| The body's action nodes — owned `action` usages, `perform` steps and nested `analysis` usages — are the steps of the case, lowered as one `Block` over the `ActionGraph` they state: `then` successions, `first`, forks, joins, decisions and merges through `ToActionGraph` (`Block.Stated`), and declaration order where the body states no succession, as a calc body orders its steps | `lower/case_body.go` `PerformsSteps`, `caseSteps`, `CaseFlowStart`; `lower/calc_body.go` `CalcBody`; `lower/block_graph.go` `IsCaseNode`, `lowerBlockFlowWith`, `recordNodeScope` | `analysis_steps_then_sequenced.sysml` (+ `.trace.golden`), `analysis_steps_first_then_reorders.sysml` (+ `.trace.golden`), `analysis_step_perform.sysml`, `analysis_step_typed_action.sysml`, `analysis_nested_step_then.sysml` | ✅ Faithful | +| The body's action nodes — owned `action` usages, `perform` steps and nested `analysis` usages — are the steps of the case, lowered as one `Block` over the `ActionGraph` they state: `then` successions, `first`, forks, joins, decisions and merges through `ToActionGraph` (`Block.Stated`), while a body stating no succession leaves its steps unordered, as an action definition's subactions are (`Cases.sysml` `Case::subcases`/`actions` `:> subactions`), so `explore`, `-engine check`, replay and seeded runs interleave them; its locals are bound before the steps and its results evaluated after them | `lower/case_body.go` `PerformsSteps`, `caseSteps`, `CaseFlowStart`; `lower/calc_body.go` `CalcBody`; `lower/block_graph.go` `IsCaseNode`, `recordNodeScope`; `lower/action_starts.go` `unorderedSubactions`; `runtime/action_subflow.go` `scheduleSubflowStep` | `analysis_steps_then_sequenced.sysml` (+ `.trace.golden`), `analysis_steps_first_then_reorders.sysml` (+ `.trace.golden`), `analysis_step_perform.sysml`, `analysis_step_typed_action.sysml`, `analysis_nested_step_then.sysml`, `analysis_explore_step_order.sysml` (+ default and `seed-1` trace goldens), `verification_explore_step_order.sysml`, `analysis_case_step_order_commuting.sysml`, `analysis_case_step_order_stated.sysml`; `case_step_order_test.go`; `robustness_case_step_order_test.go` | ⚠️ Approximate (`declared` and `reverse` (the default) perform unordered steps in declaration order, a tool-defined linearization; `explore`, `-engine check`, replay and seeded runs reach every interleaving) | | Each step is a subperformance run by the action executor, and a later step reads an earlier one's output by `step.pin`; a nested `analysis` usage runs as a step through the same dispatch, its outputs read as features of it | `runtime/calc_statements.go` `calcStmtHost.attachPerformances`, `calcStmtHost.performNode`, `calcStmtHost.runFlow`, `calcStmtHost.assignAround`; `runtime/action_statements.go` `performances.performNode`; `runtime/action_executor.go` (token traversal, data flow, deadlock detection, step budget) | `analysis_steps_then_sequenced.sysml`, `analysis_nested_step_then.sysml`, `analysis_nested_default_subject.sysml`, `analysis_robustness_test.go:failing_step`, `:step_budget`, `:deadlocked_body`, `:cyclic_successions`, `:unordered_steps` | ✅ Faithful | | A step waiting on the clock (`action sail accept after 60.0 [s];`, `accept at`) advances the run's time as an action's does: a case run on its own (`RunAnalysis`, `RunVerification`, `-analysis`) drives the clock through its flow, which is on the clock for the run and off it afterwards, whether the run ends or fails; a case an action body performs as a step (`verification run : Scene;`) pauses that body, whose executor lists the case's waits among its own and resumes the step when the instant comes, so a wait for a message nothing posts deadlocks the performing action (`ErrAcceptDeadlock`) and a wait under a behavior holding the clock is refused (`ErrStateBehaviorWaits`). An expression reading a case's output (`assign x := station.voyage.total;`) takes the case whole, so a wait under it is the typed `ErrCaseReadWaits` naming the wait, the clock left where it was | `runtime/calc_usage.go` `Context.runCalcUsage` (a run with no body to pause puts the flow on the clock), `Context.finishCalcUsage`, `Context.caseReadWaits`; `runtime/case_step.go` `performances.performCase`, `caseStepFrame`; `runtime/action_body_run.go` `bodyWait.waiter`, `Token.pausedWaiter`, `Token.hostedFlow`; `runtime/action_executor.go` `ActionExecutor.pausedWaiters`, `hostedFlows`, `armedWaits`, `visibleArmedWaits`, `hasDuePausedWork`; `runtime/snapshot.go` `executorCaptures.captureBody` | conformance `analysis_steps_wait_on_clock.sysml` + trace golden, `action_case_step_waits_on_clock.sysml` + trace golden; `robustness_case_clock_wait_test.go:TestRuntimeRobustnessCaseClockWait` | ✅ Faithful | | `return x = expr;`, the body's trailing result expression, and `out p : T = expr;` are evaluated in the case's frame after the steps complete, reading the subject, the `in` parameters, the steps' outputs and calling `calc def`s; units are preserved; an output never bound, one reading itself and outputs reading each other are typed refusals | `runtime/analysis_run.go` `calcRun.outputValues`; `runtime/calc_usage.go` `calcRun.value`, `calcRun.output`, `calcShape.resultOutput` | `analysis_return_vs_out.sysml`, `analysis_units_preserved.sysml`, `analysis_robustness_test.go:output_never_bound`, `:usage_reading_its_own_output`, `:outputs_reading_each_other` | ✅ Faithful | @@ -540,7 +540,7 @@ checked after the result is bound is not a form the runtime offers, and none is | The conditions of a nested constraint (`assert constraint [name] { }`) are the conditions of the member stating it | `parser/behavior.go` `tryParseNestedConstraint`, `condition.go` `appendConditions` | `parser/behavior_require_member_test.go:TestConstraintMemberNestedBody` | ✅ Faithful | | A constraint body that declares parameters (`constraint c { in x : Real; assert x >= 0; }`) states conditions like any other, including conditions that start with a keyword (`assert true`, `assert not false`, `assume null != x`, `assert if …`); a condition missing its expression is a diagnostic | `parser/behavior.go` `atConstraintCondition`, `parser/expr.go` `exprStartKeywords` | `parse/constraint_parameterised_conditions.golden`, `parser/constraint_condition_test.go`, `parser/negative_test.go:constraint_params_assert_no_condition` | ✅ Faithful | | A constraint carrying no condition yields no verdict (`ErrNoConditions`) rather than a vacuous pass | `errors.go` `ErrNoConditions`, `context.go` `EvaluateConstraintOn`/`EvaluateRequirementOn` | `runtime/constraint_test.go:TestConstraintWithoutConditionsIsNotAVerdict` | ✅ Faithful | -| A constraint body's action statements (`assign`, `if`, `while`/`loop`/`for`, `send`, `terminate`, `perform` — SysML v2 7.20, whose constraint body is a `CalculationBody`), action nodes and successions parse, but a constraint stating one yields no verdict (`ErrStatementNotExecuted`) and no solver query rather than one that ignored the step; a case's own steps are its procedure and are not refused | `condition.go` `appendConditions`/`statementKeyword`, `analysis.go` `CaseConditionsOf`, `solve/translate.go` `translator.condition` | `runtime/constraint_test.go:TestConstraintBodyStatementIsNotAVerdict`/`TestConstraintBodyPerformIsNotAVerdict`/`TestConstraintBodyActionFlowIsNotAVerdict`, `solve/translate_test.go:TestBodyStatementRefuses`/`TestPerformedActionRefuses`/`TestActionFlowRefuses`, `solve/objective_test.go:TestCaseStepsAreNotBodyStatements` | ⚠️ Approximate (the statements are not executed before the conditions are evaluated) | +| A constraint body's statements (`assign`, `if`, `while`/`loop`/`for` — SysML v2 7.20, whose constraint body is a `CalculationBody`) are steps of one Boolean performance, performed before its conditions are evaluated in the state they left, the ones no `then` between them orders unordered (the library leaves them so; see the unordered-statement row of the action map, which states how `explore`, `-engine check`, replay and seeded runs reach each order); locals live in a fresh frame per check, a body-local `attribute` declared with them is a local too, the constraint's own parameters are copied into the frame so a write binds the copy alone, and `SysML v2 §7.17.9`'s implicit target is the performance itself — so `assign n := …` for a name it holds no feature for (`ErrConstraintExternalAssignment`) and every effect reaching outside it (a chained or qualified write, `send`, `perform`, `terminate` — `ErrConstraintEffect`) are refused, as are the stated successions `first`/`fork`/`join`/`merge`/`decide`/`done` and a `then` reaching outside the body (`ErrStatementNotExecutable`; a `then` between two of the body's own statements orders them), a steps-only body (`ErrNoConditions`), and a case's own steps, which are its procedure and are not refused | `lower/constraint_body.go` `ConstraintStep`, `condition.go` `appendConditions`/`appendMemberConditions`/`appendOwnedConditions`/`evaluateStepsConditions`/`conditionHoldsAt`, `constraint_statements.go` `constraintStmtHost`/`runConstraintSteps`, `analysis.go` `CaseConditionsOf`, `solve/translate.go` `translator.condition` | `runtime/constraint_test.go:TestConstraintBodyStepsRun`/`TestConstraintBodyPerformIsRefused`/`TestConstraintBodyActionFlowIsRefused`/`TestConstraintBodySuccessionIsRefused`, `runtime/robustness_constraint_body_steps_test.go:TestRuntimeRobustnessConstraintBodySteps`, conformance `constraint_body_steps` + four `constraint_body_steps_*_rejected`, `solve/translate_test.go:TestBodyStatementRefuses`/`TestPerformedActionRefuses`/`TestActionFlowRefuses`, `solve/objective_test.go:TestCaseStepsAreNotBodyStatements` | ⚠️ Approximate (`declared` and `reverse` run the steps in declaration order, a tool-defined linearization, while `explore`, `-engine check`, replay and seeded runs reach every order the library admits; effects outside the performance and stated successions other than `then` between the body's own steps are refused rather than performed) | ### Instantiation and Feature Values (SysML v2 §7.6 Feature Values, KerML §8.3) @@ -556,7 +556,7 @@ checked after the result is bound is not a form the runtime offers, and none is | A multi-valued feature holds its default's contents; a single value written on it is the collection's one element | `instance.go` `GetFeatureValue` | `runtime/instance_test.go:TestMultiValuedDefaultMaterializes`, `repl/instance_test.go:TestCollectionFeatureValuesShowTheirContents` | ✅ Faithful | | A nested part usage with a body of its own is instantiated as that usage, so what its body declares wins over what its type declares, and an untyped nested part (`part engine { ... }`) still materializes | `instance.go` `compositeType`, `GetFeatureValue` | `instance_nested_usage_body.sysml`, `instance_unnamed_redefinition.sysml`, `runtime/instance_test.go:TestNestedUsageBodyOverridesItsType`, `TestUntypedNestedPartMaterializes` | ✅ Faithful (both the named form and an unnamed `:>> power = 250.0;`, which takes the name of what it redefines — see the KerML 7.3.4.5 row above) | | A single-bound multiplicity is both bounds, unless it is unbounded: `[*]` is `0..*`, so a `[*]` feature materializes empty like `[0..*]` (KerML 1.0 §8.2.5.11, confirmed by OMG issue KERML11-204) | `semantics/multiplicity.go` `multiplicityRange` | `semantics/multiplicity_test.go:TestMultiplicitySingleBoundStar`, conformance `multiplicity_unbounded_single_bound`, `parse/multiplicity_unbounded_and_subsetting.golden`, `passes/constraint_test.go:TestConstraint_RedefinitionUnboundedMultiplicity` (a `[*]` redefinition keeps an inherited `0..*` and loosens an inherited `1..*`) | ✅ Faithful | -| A required lower bound is a count of values, not an allocation: a feature whose multiplicity has lower bound n holds at least n values in every instance (KerML 1.0 §7.4.12 Multiplicities — a multiplicity is the range the number of a feature's values must lie in; KerML 1.1 §7.3.4.1), and nothing in KerML 1.0 or SysML v2.0 says when the objects that are those values come to exist. So `part p : C[5000]` holds 5000 values and `C[k * 1000]` or `C[n..*]` holds the number its lower-bound expression evaluates to in the declaring feature's scope (the library declares a bound an expression, `MultiplicityRange::bounds`, so semantic validation requires it model-level evaluable to a Natural). Past 1000 anonymous members the remainder is held as reserved identities: `size(p)`, `isEmpty`, `includes`/`excludes` of a member answer from the count and the identities, `p#(i)`, a feature chain through one member, a write and a binding make only the member reached, under the identity reserved for it, and the same member is the same object on every read. Reading every value — iteration, `all T`, a roll-up, a quantifier, listing, the explorer's state spelling of what is made — reaches each one and is charged to the element budget (`ErrElementLimitExceeded`, see *Runtime bounds*). A lower bound of `*` (`[*..*]`) requires no finite number of values, so no instance can hold it: `ErrInfiniteLowerBound`, a multiplicity violation naming the clause | `instance.go` `materializeCompositeCollection`, `holdsLazily`; `shape.go` `extractMultiplicity` (a bound left open unscoped is evaluated in the declaring feature's scope); `required.go` `requiredMembers`, `Context.withRequired`, `requiredMember`, `requiredAt`, `heldInFull`, `HeldElements`, `ElementAt`, `MadeElements`; `value.go` `Sequence.Size`, `Sequence.At`; `collections.go` `holdsValue`; `extent.go` (a namespace usage's population); `check_state.go` `stateSpeller` (made members by path, the unmade counted); `held_image.go`, `carry.go` `carriedRequired`, `adopt.go` (a snapshot, an image and an adopted model keep the reserved identities); `objref/walk.go` `CollectionElements`, `protoconv`/`engine`/`grpc`/`repl`/`queryexec` listings through `HeldElements`; `errors.go` `ErrInfiniteLowerBound` | `robustness_lazy_required_test.go:TestRuntimeRobustnessLazyRequired` (`large_lower_bound_counts_without_making`, `indexed_read_makes_one_stable_object`, `lower_bound_given_by_an_expression`, `infinite_lower_bound_names_the_clause`, `reading_every_value_past_the_element_budget`, `:within_the_element_budget`, `a_billion_required_values`, `snapshot_restores_unmade_members`, `image_carries_the_population`, `listing_every_value_is_budgeted`, `namespace_usage_held_lazily`, `written_member_holds_the_write`, `subsetter_is_among_the_values`, `quantifier_reaches_every_value`, `state_key_spells_the_population`, `every_feature_of_the_holder_is_read`, `conditional_keeps_the_count`, `replaced_population_releases_its_members`), `repl/object_args_test.go:TestCompletionThroughUnmadeRequiredMemberMakesNone`, `grpc/robustness_lazy_required_test.go:TestGRPCRobustnessLazyRequired`, `robustness_test.go:multiplicity_infinite_lower_bound`, `:multiplicity_lower_bound_too_large` (5000 values, few objects made), conformance `lazy_required_lower_bound` (`[2000]`, `[k * 1000]` with a subsetting member, roll-ups, identity), `lazy_required_lower_bound_calc` with its trace, `parse/multiplicity_expression_lower_bound.golden`, `grpc/convert_test.go`, pilot-exec-diff `w6d:lower-bound-three`, `:lower-bound-unbounded`, `:star-only`, `:held-lower-three-no-value` | ✅ Faithful (the count, the identities and every value read agree with eager creation. Standards-silent and chosen here: which populations are held lazily — past 1000 anonymous members, and only where the members' type starts no behavior, so a classifier behavior or lifetime still starts in the order eager creation gives it; a type that does start one is made in full, under the element and object budgets. Unrefereeable: the pinned artifact answers the declaration node rather than a value for every valueless required-lower-bound feature above) | +| A required lower bound is a count of values, not an allocation: a feature whose multiplicity has lower bound n holds at least n values in every instance (KerML 1.0 §7.4.12 Multiplicities — a multiplicity is the range the number of a feature's values must lie in; KerML 1.1 §7.3.4.1), and nothing in KerML 1.0 or SysML v2.0 says when the objects that are those values come to exist. So `part p : C[5000]` holds 5000 values and `C[k * 1000]` or `C[n..*]` holds the number its lower-bound expression evaluates to in the declaring feature's scope (the library declares a bound an expression, `MultiplicityRange::bounds`, so semantic validation requires it model-level evaluable to a Natural). Past 1000 anonymous members the remainder is held as reserved identities: `size(p)`, `isEmpty`, `includes`/`excludes` of a member answer from the count and the identities, `p#(i)`, a feature chain through one member, a write and a binding make only the member reached, under the identity reserved for it, and the same member is the same object on every read. Reading every value — iteration, `all T`, a roll-up, a quantifier, listing, the explorer's state spelling of what is made — reaches each one and is charged to the element budget (`ErrElementLimitExceeded`, see *Runtime bounds*). A lower bound of `*` (`[*..*]`) requires no finite number of values, so no instance can hold it: `ErrInfiniteLowerBound`, a multiplicity violation naming the clause | `instance.go` `materializeCompositeCollection`, `holdsLazily`; `shape.go` `extractMultiplicity` (a bound left open unscoped is evaluated in the declaring feature's scope); `required.go` `requiredMembers`, `Context.withRequired`, `requiredMember`, `requiredAt`, `heldInFull`, `HeldElements`, `ElementAt`, `MadeElements`; `value.go` `Sequence.Size`, `Sequence.At`; `collections.go` `holdsValue`; `extent.go` (a namespace usage's population); `namespace_binding.go` `resolveNamespaceClass` (a bound class's shared lower bound), `namespacedSubsetObjects` (a collection's own fill past its subsetters' values); `check_state.go` `stateSpeller` (made members by path, the unmade counted); `held_image.go`, `carry.go` `carriedRequired`, `adopt.go` (a snapshot, an image and an adopted model keep the reserved identities); `objref/walk.go` `CollectionElements`, `protoconv`/`engine`/`grpc`/`repl`/`queryexec` listings through `HeldElements`; `errors.go` `ErrInfiniteLowerBound` | `robustness_lazy_required_test.go:TestRuntimeRobustnessLazyRequired` (`large_lower_bound_counts_without_making`, `indexed_read_makes_one_stable_object`, `lower_bound_given_by_an_expression`, `infinite_lower_bound_names_the_clause`, `reading_every_value_past_the_element_budget`, `:within_the_element_budget`, `a_billion_required_values`, `snapshot_restores_unmade_members`, `image_carries_the_population`, `listing_every_value_is_budgeted`, `namespace_usage_held_lazily`, `written_member_holds_the_write`, `subsetter_is_among_the_values`, `quantifier_reaches_every_value`, `state_key_spells_the_population`, `every_feature_of_the_holder_is_read`, `conditional_keeps_the_count`, `replaced_population_releases_its_members`), `repl/object_args_test.go:TestCompletionThroughUnmadeRequiredMemberMakesNone`, `grpc/robustness_lazy_required_test.go:TestGRPCRobustnessLazyRequired`, `robustness_test.go:multiplicity_infinite_lower_bound`, `:multiplicity_lower_bound_too_large` (5000 values, few objects made), conformance `lazy_required_lower_bound` (`[2000]`, `[k * 1000]` with a subsetting member, roll-ups, identity), `lazy_required_lower_bound_calc` with its trace, `parse/multiplicity_expression_lower_bound.golden`, `grpc/convert_test.go`, pilot-exec-diff `w6d:lower-bound-three`, `:lower-bound-unbounded`, `:star-only`, `:held-lower-three-no-value` | ✅ Faithful (the count, the identities and every value read agree with eager creation. Standards-silent and chosen here: which populations are held lazily — past 1000 anonymous members, and only where the members' type starts no behavior, so a classifier behavior or lifetime still starts in the order eager creation gives it; a type that does start one is made in full, under the element and object budgets. Unrefereeable: the pinned artifact answers the declaration node rather than a value for every valueless required-lower-bound feature above) | | The values of a subsetting feature are values of the feature it subsets, so a nested part declared `part a : Sub :> subsystem` or `part a : Sub subsets subsystem` is one of the objects `subsystem` holds and a roll-up over `subsystem` sums over it (KerML 1.0 §7.3.4.4); a `default` is only what the feature holds where nothing else populates it (KerML 1.0 §7.3.4.5), so `part subcomponents : C [*] default null;` with `part a : Leaf :> subcomponents;` holds `a`, an inherited or redefined (`:>> a`) subsetting member keeps its membership, `totalMass = mass + sum(subcomponents.totalMass)` rolls up recursively through nested stacks, a `default null` collection nothing subsets stays empty, an explicit non-default binding stays authoritative, more members than the multiplicity admits is `ErrMultiplicityViolation`, and features subsetting each other (`xs :> ys default null; ys :> xs default null;`) report `ErrCyclicFeatureValue` rather than recurse | `subsetting.go` `subsettingContributions`/`relatedFeatureNames`, `instance.go` `GetFeatureValue` (`valueBinds` + `DefaultIsFallback`, then `holdContributed`), `shape.go` `EffectiveFeature.DefaultIsFallback` | conformance `feature_chain_rollup_over_subsets`, `cubesat_mass_rollup`, `instance_empty_aggregate_subsetting_rollup` (stack of two leaves is `210 [kg]`, a tower over the stack `311 [kg]`, the cycle), `subsetting_test.go:TestDefaultNullCollectionHoldsTheMembersSubsettingIt` (type-level, usage-level, inherited, redefined, nested, explicit binding, unsubsetted), `:TestDefaultIsFallbackOnlyWhereWrittenDefault` (a scalar `default null` with one subsetter holds it; two are a multiplicity violation), `robustness_test.go:mutually_subsetting_features`, `:cyclic_subsetting_of_default_collections`, `queryexec/derived_test.go:TestExecuteProjectsEmptyQuantitySumsAndDefaultedSubsettedParts`, `docrender/markdown_test.go:TestMarkdownRollupReport`, pilot-exec-diff `w6d:subsetting-rollup` (the reference rolls up the same `1.0`), `:subsetting-defaulted-count`, `:subsetting-two-count`, `:subsetting-none-count` (disagree, unrefereeable: the pilot counts an undefaulted `[*]` collection as `1` whether two parts subset it or none does, and folds a `default null` one to `0`, so it reads the feature as one value rather than as the objects it holds — see [pilot-execution-referee.md](pilot-execution-referee.md)) | ⚠️ Approximate (self-assessed, the reference deciding no membership; contributions are the subsetting features declared on the same object; the subsetted collection is read-only with respect to them, and a subsetting feature declared elsewhere for the same object contributes nothing; the members' order in the collection is declaration order over the flattened type, which the spec leaves unordered) | | A bracket multiplicity is the population, not a factor: `part cell : Component[4]` holds four objects, each carrying its own feature values, so a roll-up such as `sum(cell.basicMass)` counts each object once and a per-object value multiplied by the same `4` double counts. The subsetting features are among the population and anonymous objects make up the rest of the required lower bound | `instance.go` `GetFeatureValue` (`subsettingContributions`, then `mult.Lower.Value - len(contributed)` anonymous objects), `collections.go` `sum` | conformance `multiplicity_rollup_counts_each_instance_once` (a `[4]` collection, and a `[3]` one whose named subsetting part is joined by two anonymous objects), `feature_chain_rollup_over_subsets`, `cubesat_mass_rollup` | ✅ Faithful | | A redefining feature is the feature it redefines, so `part subsystems : Component[*] :>> Subsystems` makes both names read one collection, and a chain of redefinitions (`dry :>> own :>> mass`) reads one feature value under every name even when a usage restates the redefinition (`part sat : Sys { attribute :>> own = 10.0; }`) (KerML 1.0 §7.3.4.5) | `subsetting.go` `aliasRedefinedFeatureValues`, `redefinitionGroups`, `sharedRedefinitionName`, `redefinedNames`, `isFeatureOf` | conformance `cubesat_mass_rollup`, `redefinition_restated_in_a_usage`, `redefinition_multilevel_base_name`, `redefinition_value_under_either_name`, `redefinition_valued_under_two_names`, `robustness_test.go:one_feature_valued_under_two_names` | ✅ Faithful (the shared feature value is the one the most specific declaration writing a value created, whichever name it wrote — the redefining name, the base name, or a name in between, including where the value is written in an abstract part usage a configuration specializes; one declaration valuing two names of the feature is `ErrConflictingRedefinition` rather than a silent pick) | @@ -704,13 +704,17 @@ checked after the result is bound is not a form the runtime offers, and none is | An action node reached over several successions is one performance that follows all of them: a step with no declared multiplicity holds one value (KerML 1.0 §7.4.5), and each succession into it is a `HappensBefore` link (Kernel Semantic Library `Occurrences.kerml`) whose later occurrence is that performance | `runtime/action_executor.go` `synchronize` — the join row's one gate, applied from `stepToken` to every node kind but a merge (`synchronizes`; `Actions::MergeAction` passes each arrival on) — with `awaitedSuccessions`, `reachableFrom` and `leaves` deciding which successions a plain node awaits; `runtime/action_subflow.go` `Token.positionIn` (a token in a nested flow stands at the node performing it) | `conformance/action_node_with_two_incoming_successions_runs_once.sysml` + trace golden, `action_nested_node_two_successions_per_performance.sysml` + trace golden (two performances of an action holding such a node each perform it once), `action_node_concurrent_performances` + trace golden and `action_node_concurrent_nested_bindings` + trace golden (a flow-owning node reached from two fork branches performs once, each flow at its own pin), all derived in [the semantic oracle](behavior-semantic-oracle.md); `action_node_converges_after_decision` + trace golden (a plain node behind a decision's two branches performs once for the branch taken, no deadlock) and `action_node_loop_back_reperforms` + trace golden (a plain node a loop re-enters performs once per pass), with `action_decision_else_done`, `action_guard_reads_calc_usage`, `action_succession_guard_fork_branch_pruned` and `f63_control_node_body` pinning the same at final nodes and under fork guards; `robustness_test.go:deadlock_join_starvation`, `:deadlock_join_same_succession_twice` pin that a join, unlike a plain node, awaits an unreachable source (`ErrActionDeadlock`) | ✅ Faithful (`hits = 1`: the tokens arriving over the node's successions collapse into one performance, per performance of the owning action. A join awaits every incoming succession, its sources being 1..1; a plain node awaits a succession only once it has delivered or while some token of the activation, other than one held at the node, can still reach its source without passing through the node or through a join that node must feed — a decision branch whose guard did not hold, or a succession back from a node downstream of this one, orders no performance before this one, so a loop through a plain node re-performs it once per iteration rather than deadlocking) | | Decision node (guarded branching): SysML v2 §7.17.3 rule 2 gives outgoing successions target multiplicity 0..1; `Actions::DecisionAction` selects exactly one outgoing `HappensBeforeLink` | `action_executor.go` `stepDecisionNode`, `probeGuard` (guards are evaluated in order until one holds, as `enabledSuccessions` evaluates them out of any other node; the ones after it are read in a `beginProbe` preview that restores the budget, the trace, every effect and the object identities the preview took; the first that holds is taken, an unguarded succession is the fallback when none holds; a previewed guard that fails to evaluate is no alternative and no error, recorded as a `guard-unevaluable` note by `noteUnevaluableGuard`), `action_choice.go` `noteDecisionBranches` (several holding guards are a choice point naming the branches by position and target) | `conformance/action_decision_merge_guarded_branch.sysml` + `.expected.json` + trace golden; `conformance/action_choice_decision_overlapping_guards.sysml` + `.expected.json` (both outcomes listed as admissible) + trace golden; `conformance/action_choice_unevaluable_guard.sysml` + `.expected.json` + trace golden; `choice_test.go:TestChoicesResetPerRun`, `:TestLaterGuardErrorIsNotAChoiceNorAFailure`, `:TestLaterGuardIsProbedWithoutCost`; `grpc/choice_test.go:TestExecuteAction_UnevaluableGuardDiagnostics`; `repl/choice_test.go:TestStepReportsUnevaluableGuards`; `robustness_test.go:decision_no_satisfied_guard`, `:decision_all_guards_false` (`ErrNoEnabledSuccession`) | ✅ Faithful (the library selects exactly one link but does not say which when several guards hold, so the pick is the executor's and is reported as one) | | Which linearization a run takes where the library orders nothing is tool-defined, so it is a named *scheduling policy* rather than one fixed rule: `reverse` (the default — reverse token-index order, first holding guard, first enabled transition — so every run recorded before policies were selectable replays unchanged), `declared` (tokens in spawn order, guards and transitions in declaration order) and `seed:` (every pick drawn from a PCG sequence the seed fixes, so one seed replays one run on every platform and two seeds may take two linearizations). The policy decides every choice point of the row above and the rows below — token order in a step, the holding guard a decision follows, the enabled transition that fires, the order the orthogonal regions one event enables react in, whose same-step write stands (it follows from the token order) — and nothing else: every choice point a run reaches is reported under every policy, and the `took …` of each is what the policy took (another linearization may reach other choice points, so their count is not fixed across policies). A spelling naming no policy (`seed`, `seed:`, `seed:-1`, `seed:abc`, an unknown name) is `ErrInvalidSchedulePolicy` before anything runs, on every surface: `sysml -schedule`, `%schedule`, and the `schedule` field of `ExecuteActionRequest`, `ExecuteStateRequest` and `RunAnalysisRequest` (`INVALID_ARGUMENT`; the `schedule` capability advertises the field). Guards read in a preview leave the seeded sequence where it was, so reporting a choice does not change which alternative the run takes; a decision branch picked past the first was only previewed, so the run reads its guard once more before taking it (what evaluating it materialized or derived is then the run's), as `fireTransition` reads a transition's guard again before it fires; an executor a debugger drives call by call keeps the scheduler its run started with when another run under another policy is driven to completion in between, and `Decide` previews the transition that run would fire from that same scheduler, putting its draw back; and a composite state every leaf of its orthogonal regions reaches is asked for its transition once per dispatch or change poll, so the choice among its enabled transitions draws once (one candidate, one `took …`), a composite state a nested state outranks draws nothing, the pick among a candidate's enabled transitions being made only once it survives conflict resolution, so the run's next reported choice takes the seed's next draw; and a token parked at a join its other branches have not reached or at an accept no message in flight answers keeps its place in the step's order, so a seed draws only among the tokens able to act and steps taken while every token is parked draw nothing | `runtime/scheduler.go` `SchedulePolicy`, `ParseSchedulePolicy`, `SchedulePolicyError`, `scheduler.orderTokens`, `scheduler.pick`, `scheduler.mark`; `runtime/context.go` `SetSchedule`, `scheduling`, `beginExecutorRun` (a run under way keeps the scheduler it started with, across the other runs driven while it is paused), `previewExecutorRun` (`Decide` previews under it); `action_executor.go` `scheduleTokens`, `parked` (the tokens `Step` and a nested flow in `action_subflow.go` move, in the policy's order, a seed ordering only those able to act), `stepDecisionNode`; `state_executor.go` `enabledTransitions`, `selectCandidates`, `chooseTransition`, `chooseRegion` (the next region to react, drawn from the same `scheduler.pick` and reported whenever two or more can); `state_change_trigger.go` `risenChangeTransitions`; `cmd/sysml/main.go` `schedulePolicy` flag; `repl/schedule.go` `doSchedule`; `grpc/service.go`, `grpc/analysis.go` (`CapabilitySchedule`); `client/opensysml/execute.go` `WithSchedule`, `analysis.go` `Schedule`; `client/python/opensysml/connection.py` `schedule=` | `scheduler_test.go:TestParseSchedulePolicy`, `:TestParseSchedulePolicyRejectsUnknownSpellings`, `:TestReverseAndDeclaredOrders`, `:TestSeededSchedulingIsReproducible`, `:TestSeededTransitionChoiceMatchesTheRun`, `:TestProbeLeavesTheSeededSequenceInPlace`, `:TestPickedGuardIsReadByTheRun`, `:TestDrivenRunKeepsItsSchedulerAcrossOtherRuns`, `:TestDecidePredictsTheDrivenRunsTransition`, `:TestSharedAncestorChoiceDrawsOnce`, `:TestOutrankedChoiceDrawsNothing`, `:TestParkedTokensDrawNothing`, `explore_test.go:TestExploreSiblingRegionOrder` (`reverse` and `declared` take region declaration order and report the choice; `seed:1` and `seed:8` reach the two effect orders, each replaying), `choice_test.go:TestNotesOfATransitionBlockedBeforeFiringAreDropped`; `conformance_test.go:TestExecutionConformance` (the whole suite under the default, every `.expected.json` and `.trace.golden` unchanged), `:TestExecutionConformanceUnderPolicies` (the whole suite under `declared` and `seed:1`: no error, panic or hang, and every case pinning no policy and listing no `outcomes` produces its default outputs), `trace_test.go:TestExecutionTrace` (`.declared.trace.golden` and `.seed-1.trace.golden` for every `outcomes` case); `conformance/action_choice_shared_message_accept` lists `outcomes` (`a = 2, b = 1` and `a = 1, b = 2`, derived in `behavior-semantic-oracle.md` § Two accepts of one type racing for two sends) with a `.trace.order` and `declared`/`seed-1` trace goldens; `send_identity_same_named_ports` runs unpinned (it pinned `"schedule": "reverse"` while the via-less `accept Ping` over-matched a transfer addressed to `alpha.inPort`; with the accept held to the receiver the transfer reaches, `waiting` has one enabled transition under every policy); `cmd/sysml/run_test.go:TestRunActionUnderSchedule`; `repl/schedule_test.go`; `grpc/schedule_test.go`, `grpc/capability_test.go`; `client/opensysml/schedule_test.go`, `schedule_internal_test.go`; `client/python/tests/test_schedule.py`; `conformance/scenarios/06-behavior.json` (`schedule` on the wire, in-process and over every transport) | ✅ Faithful (the three driven policies each take one linearization per run and say which at every choice point; `explore`, the bounded exhaustive replay of the row below, enumerates them all, and the harness checks every listed outcome is reachable and no unlisted one is, so a fixed order is never passed off as the only one. Same-step write order is not a pick of its own: it follows the token order, so under a policy that orders the writing tokens differently the other write stands, which is what the write-conflict goldens under `declared` and `seed:1` pin) | -| Every linearization the library admits is a valid run, so a model with choice points has a *set* of outcomes, not one: the `explore[:runs=N,depth=D]` policy enumerates it by replay — one run records the alternative each choice point took; each later run is a fresh context on the same lowered model (its own instances, message bus, clock, object behaviors, calc memo and notes — nothing of one run is seen by the next; every run of one exploration is built over that exploration's own resolver and semantic model, an `analysis.Worker` over the shared frozen index, so two explorations on one model, or one beside a gRPC request or a REPL completion, share nothing that memoizes) that follows the recorded prefix and takes the next untried alternative at its end, the prefixes run in plan order — every one departing from the first run at one choice before any departing at two, earliest choice first, so a `runs` budget of one more than the first run's choice points varies each of them once — until no alternative is untried or a budget is hit (a choice point past `depth` takes its first alternative in every run and is never varied). An action step under `explore` is one token advancing one node — the fixed policies move every steppable token once per step — so the tokens able to act are picked among afresh after each move, a branch of several nodes can be overtaken by a concurrent one between any two of them, and a `complete` exploration covers every interleaving of the nodes the library leaves unordered, at body granularity (a body's statements are not interleaved). Runs agreeing on the observables the conformance harness compares — an action's outputs; a state machine's final state, states visited and values; an analysis case's outputs and verdicts — are one canonical outcome, counted by the linearizations reaching it and witnessed by one run's choice sequence (an object is compared by its type and feature values through the run's own context, never by the id that run gave it, so equal objects with different ids are one outcome and different objects under one id are two); a run that fails is an outcome of its own (`error: …`), not the end of the exploration; a behavior with no choice point explores in exactly one run; the same model explores to the same sorted table every time. The budget (1024 runs and 64 choice points per run by default) is never exceeded silently: `incomplete: budget hit after N runs` names each budget hit, `runs` before `depth`. `explore` is not a policy one context runs under (`ErrExploreUndriven` from `SetSchedule`), so a step-by-step debugger cannot explore: `%schedule explore` is a typed error at the prompt while `sysml -schedule explore` (`-action`, `-state`, `-analysis`, `-calc`; `-engine explore` is the same selection) tables the outcomes, exit status `2` when incomplete, and the wire answers `outcomes` and `exploration` on `ExecuteActionResponse`, `ExecuteStateResponse` and `RunAnalysisResponse` under the `schedule_explore` capability. A malformed spelling (`explore:`, `explore:runs=0`, `explore:depth=-1`, `explore:bogus`, an option twice) is `ErrInvalidSchedulePolicy` on every surface. The harness explores every case listing `outcomes` and fails when a listed outcome is unreachable or an unlisted one is reached, naming the outcome and a witness, and when the budget is hit, telling the author to raise `exploreBudget`; cases without `outcomes` are not explored by default | `runtime/explore.go` `Explore`, `ExploreBudget`, `DefaultExploreBudget`, `ExploredOutcome`, `Exploration.Status`, `ChoiceTaken`, `FormatChoices`, `exploreRun.pick`/`beginStep`/`resolve`/`nextPrefix` (the replayed prefix and the frontier), `exploreStep.next`/`acted` (one move ends the step); `runtime/scheduler.go` `scheduler.oneMove`, `tokenSchedule.Choice`; `runtime/action_executor.go` `Step` (an exploring step picks among every token able to act, a paused body whose wait has ended among them — `action_body_run.go` `Token.resumable` — where a fixed sweep resumes paused bodies last); `runtime/outcome.go` `Outcome.identity` (the canonical identity, every name quoted so no output name can spell another outcome), `Outcome.String` and `Outcome.RenderedOutputs` (the rendering), `objectSpeller` (objects by type and feature values), `Context.ActionOutcome`, `StateExecutor.Outcome`, `Context.VerifiedOutcome`; `runtime/scheduler.go` `parseExploreOptions`, `ExplorePolicy`, `SchedulePolicy.Exploration`; `runtime/context.go` `beginExploration` (the per-run scheduler of an exploring run), `SetSchedule` (`ErrExploreUndriven`); `action_choice.go` (token order and same-step writes through the exploring run); `cmd/sysml/main.go`, `report.go` (`outcomes`, `exploration` in `-json`); `repl/explore.go` `ExploreAtPromptError`, `exploreVerdict`; `repl/schedule.go` `doSchedule`; `grpc/explore.go` `Service.explore`, `grpc/service.go`, `grpc/analysis.go` (`CapabilityScheduleExplore`); `client/opensysml/explore.go` `ExploreAction`, `ExploreState`, `ExploreAnalysis`; `client/python/opensysml/exploration.py`, `connection.py` `explore_action`, `explore_state`, `explore_analysis` | `explore_test.go:TestExploreThreeWritersReachEveryOutcomeOnce`, `:TestExploreIsDeterministic`, `:TestExploreNoChoicePointsIsOneRun`, `:TestExploreRunsBudgetIsIncomplete`, `:TestExploreDepthZeroIsIncomplete`, `:TestExploreErrorIsAnOutcome`, `:TestExploreDecisionInLoop`, `:TestExploreStateTransitionConflict`, `:TestExploreRejectsOtherPolicies`, `:TestParseExplorePolicy`, `:TestExploreRunsShareNoState`, `:TestExploreTellsObjectsApartByWhatTheyAre`, `:TestExploreEquatesObjectsByWhatTheyAre`, `:TestOutcomeIdentityQuotesNames`, `:TestOutcomeIdentityOpensEveryObject` (a ring of objects is spelled once around, and objects nested past the rendering's depth still tell outcomes apart), `:TestExploreSiblingRegionOrder`, `:TestExplorePausedBodyDueIsAMove`; `explore_order_test.go:TestExploreVariesEveryChoiceOfTheFirstRunFirst` (plan order: the first run's choices varied earliest first, before any twice); `conformance_test.go:TestExecutionConformance` (`exploreConformanceCase` over every `outcomes` case); `conformance/action_explore_three_writers` (six linearizations, three outcomes, derived in `behavior-semantic-oracle.md` § Three concurrent writers of one feature: six orders, three values), `action_explore_write_between_branch_nodes` (three linearizations, three outcomes, the third reached only when one branch's two nodes both run before the other branch's one; § A write between two nodes of a concurrent branch: three orders, three outcomes), `action_explore_performed_and_accept_due_together` (six linearizations, two outcomes, the paused performed action and the sibling accept due at one instant each resumed first by three; § A performed action and a sibling accept due at one instant: which resumes first is open), `action_explore_early_race_long_tail` (20 linearizations, two outcomes, the open write order met before the closed orders of the branches' tails; § Two writers of one feature before a long tail of closed choices: two values), `action_explore_decision_in_loop` (§ A decision inside a loop: every pass is its own open choice), `state_explore_transition_conflict` (§ Two transitions out of one state enabled by one event: exactly one fires, which one is open), `state_explore_region_order` (§ Transitions in sibling regions enabled by one event: each fires, in which order is open), each with a `.trace.golden` under the default and `declared`/`seed-1` goldens; `cmd/sysml/run_test.go:TestRunUnderExplore`, `:TestJSONReportsExploration`, `spacecraft_showcase_test.go:TestExploreTablesTheSpacecraftRaceWithinItsBudget` (a race past the default depth, tabled within a stated budget at any `-jobs`); `repl/explore_test.go`, `repl/isolation_test.go` (completion answers beside an exploration; a second command waits); `grpc/explore_test.go`, `grpc/evaluate_retention_test.go` (concurrent requests on one model); `analysis/isolation_test.go` (two plans on two goroutines under `-race`, each on its own worker; a run's context takes the budget's `Steps` and `Memory` at construction, a zero field being the context's own); `client/opensysml/explore_test.go`; `client/python/tests/test_explore.py`; `client/rust/opensysml/tests/client.rs`, `client/java/.../ApiIntegrationTest.java`, `client/node/test/client.test.ts` (the `schedule_explore` capability) | ✅ Faithful (exploration is over the choice points the executors report, so a linearization two choice points do not distinguish is not run twice; a replay whose choice points differ from its recorded prefix is `ErrExplorationDiverged` rather than a table nobody can trust) | +| Every linearization the library admits is a valid run, so a model with choice points has a *set* of outcomes, not one: the `explore[:runs=N,depth=D]` policy enumerates it by replay — one run records the alternative each choice point took; each later run is a fresh context on the same lowered model (its own instances, message bus, clock, object behaviors, calc memo and notes — nothing of one run is seen by the next; every run of one exploration is built over that exploration's own resolver and semantic model, an `analysis.Worker` over the shared frozen index, so two explorations on one model, or one beside a gRPC request or a REPL completion, share nothing that memoizes) that follows the recorded prefix and takes the next untried alternative at its end, the prefixes run in plan order — every one departing from the first run at one choice before any departing at two, earliest choice first, so a `runs` budget of one more than the first run's choice points varies each of them once — until no alternative is untried or a budget is hit (a choice point past `depth` takes its first alternative in every run and is never varied). An action step under `explore` is one token advancing one node — the fixed policies move every steppable token once per step — so the tokens able to act are picked among afresh after each move, a branch of several nodes can be overtaken by a concurrent one between any two of them, and a `complete` exploration covers every interleaving of the nodes the library leaves unordered, with a leaf body yielding after its start shot and after each statement wherever another performance may change its outcome (`lower.BodyDivides`; a body's own statements keep declaration order). Runs agreeing on the observables the conformance harness compares — an action's outputs; a state machine's final state, states visited and values; an analysis case's outputs and verdicts — are one canonical outcome, counted by the linearizations reaching it and witnessed by one run's choice sequence (an object is compared by its type and feature values through the run's own context, never by the id that run gave it, so equal objects with different ids are one outcome and different objects under one id are two); a run that fails is an outcome of its own (`error: …`), not the end of the exploration; a behavior with no choice point explores in exactly one run; the same model explores to the same sorted table every time. The budget (1024 runs and 64 choice points per run by default) is never exceeded silently: `incomplete: budget hit after N runs` names each budget hit, `runs` before `depth`. `explore` is not a policy one context runs under (`ErrExploreUndriven` from `SetSchedule`), so a step-by-step debugger cannot explore: `%schedule explore` is a typed error at the prompt while `sysml -schedule explore` (`-action`, `-state`, `-analysis`, `-calc`; `-engine explore` is the same selection) tables the outcomes, exit status `2` when incomplete, and the wire answers `outcomes` and `exploration` on `ExecuteActionResponse`, `ExecuteStateResponse` and `RunAnalysisResponse` under the `schedule_explore` capability. A malformed spelling (`explore:`, `explore:runs=0`, `explore:depth=-1`, `explore:bogus`, an option twice) is `ErrInvalidSchedulePolicy` on every surface. The harness explores every case listing `outcomes` and fails when a listed outcome is unreachable or an unlisted one is reached, naming the outcome and a witness, and when the budget is hit, telling the author to raise `exploreBudget`; cases without `outcomes` are not explored by default | `runtime/explore.go` `Explore`, `ExploreBudget`, `DefaultExploreBudget`, `ExploredOutcome`, `Exploration.Status`, `ChoiceTaken`, `FormatChoices`, `exploreRun.pick`/`beginStep`/`resolve`/`nextPrefix` (the replayed prefix and the frontier), `exploreStep.next`/`acted` (one move ends the step); `runtime/scheduler.go` `scheduler.oneMove`, `tokenSchedule.Choice`; `runtime/action_executor.go` `Step` (an exploring step picks among every token able to act, a paused body whose wait has ended among them — `action_body_run.go` `Token.resumable` — where a fixed sweep resumes paused bodies last); `runtime/outcome.go` `Outcome.identity` (the canonical identity, every name quoted so no output name can spell another outcome), `Outcome.String` and `Outcome.RenderedOutputs` (the rendering), `objectSpeller` (objects by type and feature values), `Context.ActionOutcome`, `StateExecutor.Outcome`, `Context.VerifiedOutcome`; `runtime/scheduler.go` `parseExploreOptions`, `ExplorePolicy`, `SchedulePolicy.Exploration`; `runtime/context.go` `beginExploration` (the per-run scheduler of an exploring run), `SetSchedule` (`ErrExploreUndriven`); `action_choice.go` (token order and same-step writes through the exploring run); `cmd/sysml/main.go`, `report.go` (`outcomes`, `exploration` in `-json`); `repl/explore.go` `ExploreAtPromptError`, `exploreVerdict`; `repl/schedule.go` `doSchedule`; `grpc/explore.go` `Service.explore`, `grpc/service.go`, `grpc/analysis.go` (`CapabilityScheduleExplore`); `client/opensysml/explore.go` `ExploreAction`, `ExploreState`, `ExploreAnalysis`; `client/python/opensysml/exploration.py`, `connection.py` `explore_action`, `explore_state`, `explore_analysis` | `explore_test.go:TestExploreThreeWritersReachEveryOutcomeOnce`, `:TestExploreIsDeterministic`, `:TestExploreNoChoicePointsIsOneRun`, `:TestExploreRunsBudgetIsIncomplete`, `:TestExploreDepthZeroIsIncomplete`, `:TestExploreErrorIsAnOutcome`, `:TestExploreDecisionInLoop`, `:TestExploreStateTransitionConflict`, `:TestExploreRejectsOtherPolicies`, `:TestParseExplorePolicy`, `:TestExploreRunsShareNoState`, `:TestExploreTellsObjectsApartByWhatTheyAre`, `:TestExploreEquatesObjectsByWhatTheyAre`, `:TestOutcomeIdentityQuotesNames`, `:TestOutcomeIdentityOpensEveryObject` (a ring of objects is spelled once around, and objects nested past the rendering's depth still tell outcomes apart), `:TestExploreSiblingRegionOrder`, `:TestExplorePausedBodyDueIsAMove`; `explore_order_test.go:TestExploreVariesEveryChoiceOfTheFirstRunFirst` (plan order: the first run's choices varied earliest first, before any twice); `conformance_test.go:TestExecutionConformance` (`exploreConformanceCase` over every `outcomes` case); `conformance/action_explore_three_writers` (six linearizations, three outcomes, derived in `behavior-semantic-oracle.md` § Three concurrent writers of one feature: six orders, three values), `action_explore_write_between_branch_nodes` (three linearizations, three outcomes, the third reached only when one branch's two nodes both run before the other branch's one; § A write between two nodes of a concurrent branch: three orders, three outcomes), `action_explore_performed_and_accept_due_together` (six linearizations, two outcomes, the paused performed action and the sibling accept due at one instant each resumed first by three; § A performed action and a sibling accept due at one instant: which resumes first is open), `action_explore_early_race_long_tail` (20 linearizations, two outcomes, the open write order met before the closed orders of the branches' tails; § Two writers of one feature before a long tail of closed choices: two values), `action_explore_decision_in_loop` (§ A decision inside a loop: every pass is its own open choice), `state_explore_transition_conflict` (§ Two transitions out of one state enabled by one event: exactly one fires, which one is open), `state_explore_region_order` (§ Transitions in sibling regions enabled by one event: each fires, in which order is open), each with a `.trace.golden` under the default and `declared`/`seed-1` goldens; `cmd/sysml/run_test.go:TestRunUnderExplore`, `:TestJSONReportsExploration`, `spacecraft_showcase_test.go:TestExploreTablesTheSpacecraftRaceWithinItsBudget` (a race past the default depth, tabled within a stated budget at any `-jobs`); `repl/explore_test.go`, `repl/isolation_test.go` (completion answers beside an exploration; a second command waits); `grpc/explore_test.go`, `grpc/evaluate_retention_test.go` (concurrent requests on one model); `analysis/isolation_test.go` (two plans on two goroutines under `-race`, each on its own worker; a run's context takes the budget's `Steps` and `Memory` at construction, a zero field being the context's own); `client/opensysml/explore_test.go`; `client/python/tests/test_explore.py`; `client/rust/opensysml/tests/client.rs`, `client/java/.../ApiIntegrationTest.java`, `client/node/test/client.test.ts` (the `schedule_explore` capability) | ✅ Faithful (exploration is over the choice points the executors report, so a linearization two choice points do not distinguish is not run twice; a replay whose choice points differ from its recorded prefix is `ErrExplorationDiverged` rather than a table nobody can trust) | +| A leaf action body's performance is not one indivisible move: its initial values are read at its start shot (`Occurrences.kerml` `Occurrence::startShot`; KerML 1.0 §8.4.4.11 `FeatureValue` with `isInitial`), each `assign` is an `AssignmentAction` subaction whose write happens when it ends (`Actions.sysml` `assignments :> subactions`; `FeatureReferencingPerformances.kerml` `FeatureWritePerformance`), and no `HappensBefore` orders two concurrent performances, so another one may run between a body's snapshot and its assignment — `a[2] { attribute t := c; assign c := t + 1; }` admits `{c = 1, c = 2}`, `a[3]` `{1, 2, 3}`, two fork branches with that body `{1, 2}`, and likewise when each branch performs that body as a typed or performed action in an executor of its own; an `if`'s guard is evaluated before its branch (`ControlPerformances.kerml` `IfThenPerformance`, `succession ifTest then thenClause`). One initialization and one assignment stay atomic, and a succession still orders the steps it connects ([derivation](behavior-semantic-oracle.md#a-leaf-bodys-start-shot-and-its-assignments-another-performance-may-run-between-them)) | `lower/body_moves.go` `BodyDivides`, `ReadsAtStart`; `lower/body_moves.go` `BodySharesMoves`, `FlowSharesMoves`; `runtime/action_body_run.go` `usageWork.perform`, `ActionExecutor.bodyDivides`, `runBody`, `Context.yieldBody`, `Context.guardPerformed`, `Context.tokenStepBody`; `runtime/invoke_action.go` `Context.startShotMove`; `runtime/statements.go` `ifStatement`; `runtime/scheduler.go` `bodyYields`, `drawYield`; `smt/support.go` `checkBodyInterleaving` | `action_explore_body_lost_update.sysml`, `action_explore_body_three_way.sysml`, `action_explore_body_fork_lost_update.sysml`, `action_explore_body_ordered_substeps.sysml`, `action_explore_body_guard_branch.sysml`, `action_explore_body_typed_callees.sysml`, `action_explore_body_performed_callees.sysml`, `action_explore_body_own_callees.sysml`, `action_step_multiplicity_single_assignment.sysml`; `TestRuntimeRobustnessExploreBodyInterleavings` (with `testdata/robustness/action_explore_body_callee_outputs.sysml`: a callee's output writes are observed one at a time); `smt` `TestAnalyzeRefusesBodyInterleaving` | ⚠️ Approximate: `explore`, `-engine check`, replay and seeded runs reach every interleaving of concurrent performances at these boundaries, and `reverse`/`declared` run a body whole as before; the order of two unordered statements of one body is the next row's; executors running on one clock (object behaviors, state machines, actions started together) interleave by whole turns at an instant, so their moves within one instant are not interleaved; and the SMT engine refuses a dividing body as `body interleaving` | +| The direct statements of one body that no succession or control structure orders are unordered subactions of it (`Actions.sysml` `assignments`, `sendSubactions`, `ifSubactions`, `loops` `:> subactions`; `Performances.kerml` `enclosedPerformances`; nothing links two of them by `HappensBefore`): `{ assign x := 1; assign y := x; }` admits `{y = 0, y = 1}`; `then` orders the statements it links, an `if`'s guard precedes its branch and a loop's iterations follow each other ([derivation](behavior-semantic-oracle.md#direct-statements-of-one-body-no-succession-orders-each-is-performed-in-which-order-is-open)) | `lower/statement_order.go` `StatementOrder`, `BodyStatementOrder`, `keepsPlace`, `chainsStatements`; `runtime/statements.go` `stmtEngine.runUnordered`, `pickStatement`; `runtime/action_statements.go` `actionStmtHost.statementOrder`; `runtime/state_statements.go` `stateStmtHost.statementOrder`; `runtime/choice.go` `ChoiceStatementOrder`; `runtime/replay.go` (`statements in` witness lines); `smt/support.go` `checkBody` | `action_explore_statement_order_dependent.sysml`, `_chain`, `_if`, `_calc`, `_read_first`, `_terminate` (each `outcomes` the full admissible set under explore, check and the seed sweep), `_independent` and `_then` (one outcome); `lower` `TestStatementOrderReachesEachClassOnce`, `TestStatementOrderReorders`; `TestRuntimeRobustnessExploreStatementOrder` (every order explored, a budget hit reported incomplete, a recorded order replayed, seeds reproducible, `declared`/`reverse` keep declaration order); `testdata/check/por_state_do_write.sysml` (a do body's two writes reached in both orders); `smt` `TestAnalyzeRefusesStatementOrder` | ⚠️ Approximate: `explore`, `-engine check`, replay and seeded runs reach every order of a body's unordered statements, two orders that only swap adjacent independent statements being one; `declared` and `reverse` run a nested body's statements first to last, and an action definition's own statements in the token order each policy gives them (`reverse` last to first), a tool-defined linearization; `explore` has no partial-order reduction, so a body of many dependent unordered statements may exhaust its run budget, which it reports as incomplete, never proved; the SMT engine refuses two dependent unordered statements as `statement order` | +| A calc or constraint body's direct statements that no `then` orders are unordered as an action body's are (SysML v2 7.19/7.20: a `CalculationBody` is an action body; `Actions.sysml` `assignments`, `ifSubactions`, `loops` `:> subactions`), so `calc def Ord { attribute y : Integer := 1; assign y := y * 10; assign y := y + 2; y }` admits `12` and `30`; the body is performed whole inside the step that evaluates it, so its statements are reordered among themselves and never interleaved with another performance, and the result expression or condition is evaluated after them; a guard whose verdict depends on the order chooses between the distinct results the orders produce, an evaluation error among them | `lower/calc_body.go` `CalcBodyWithOrder`; `lower/constraint_body.go` `ConstraintBodyWithOrder`; `lower/statement_order.go` `CalcBodyStatementOrder`, `NextFixed`; `runtime/calc_statements.go` `calcStmtHost.statementOrder`; `runtime/constraint_statements.go`; `runtime/invocation_statement_order.go` `invokeWithStatementOrderResults` (a pure calc or constraint whose body reorders, itself or through what it calls (`runtime/order_analysis.go` `reordersTransitively`), is one choice point between the distinct results its orders produce, memoized per arguments within the outermost such invocation, so a recursive calc costs one choice, not one per level); `runtime/guard_statement_order.go` `guardUnderStatementOrders`; `runtime/statement_order_sweep.go`; `runtime/invoke_calc.go` (a compiled calc whose statements reorder is interpreted under the ordering schedules) | `conformance/calc_explore_statement_order` (+ trace goldens under the default, `declared` and `seed:1`), `_commuting`, `_declared`, `_derived`, `_recursive`, `_recursive_deep` (each two outcomes in two runs), `_then`, `_then_unordered`; `conformance/constraint_explore_statement_order`; `conformance/action_guard_statement_order`; `conformance/state_transition_guard_statement_order`; `robustness_atomic_body_order_test.go:TestRuntimeRobustnessAtomicBodyOrder`; `lower` `calc_order_test.go`, `constraint_order_test.go`; `smt` `atomic_body_order_test.go` | ⚠️ Approximate: `explore`, `-engine check`, replay and seeded runs reach every order; `declared` and `reverse` (the default) run a calc or constraint body in declaration order, as they do the body of a nested action usage, a tool-defined linearization — only an action definition's own body runs last to first under `reverse`; an order-dependent guard inside a preview, and a guard whose constraint body may write outside its performance, are reported not covered; the SMT engine refuses a body of dependent unordered statements as `statement order` | +| A terminate action usage's body is an ordinary `ActionBody` (SysML.xtext `TerminateActionUsage`), so it states successions as any action body does, and its implicit `TerminateAction::terminateOccurrence` (`Actions.sysml`) is a subaction no succession orders against the body's others: it may end the usage's performance before, between or after them, an output pin left unwritten then binding no value ([derivation](behavior-semantic-oracle.md#a-terminate-action-usages-body-and-its-implicit-terminate-each-is-performed-in-which-order-is-open)) | `lower/action_subflow.go` `lowerTerminateNode` (the body, or the flow it states as a block, then the terminate it stands for); `runtime/action_terminate.go` (a `terminate` naming the usage inside its own body ends the usage's performance) | `action_terminate_usage_body_ends_itself.sysml`, `_body_ends_itself_binds_pin`, `_in_block_names_itself`, `_with_body`, `_binds_output_pin`, `_in_block_binds_pin`, `_body_performs_action` (explored and checked to their full outcome sets, the unbound-pin multiplicity violation among them); `TestRuntimeRobustnessTerminateBodyFlow` (a stated flow runs, a `then` chain reaches the terminate early, a succession cycle and a succession to an unknown step are typed errors) | ⚠️ Approximate: `declared` and `reverse` perform the implicit terminate after the body, a tool-defined linearization; action usages written in an `if` or loop block with no succession between them keep declaration order under every policy | | A witness is a run that can be run again: the `replay:` policy reads a file of `input = ` lines — the inputs a solver chose, each as `InputTaken.String` spells them (a rational exactly, an enumeration or variation-point constructor by its qualified name), any number of them or none, so a stage-1 or `check` witness without them replays as before — followed by choice lines — each as `ChoiceTaken.String` spells them, one per line or joined by `; `, up to the first blank line, so a checker's witness file with a trace body after its header serves as it stands — fixes the inputs on the action as it starts, before its defaults and ahead of the moves, through the same path a caller's `Start` values take, refusing one naming a feature the action does not declare or cannot take with the typed `WitnessInputError` naming it, and resolves the run's choice points in that order, each line having to name the step the run is at and pick among the alternatives it offers, then as `reverse` picks, one token a step, once the lines are spent; a line the run cannot follow — a pick not offered, a step already passed — or one left over at the end fails the run with the typed `ReplayError` naming the move, its choice and what the run faced (`Context.Unfollowed`), never silently running another linearization; a file that is empty or whose lines spell no choice is `SchedulePolicyError` as the policy is parsed, while a header of `no choice points` alone is the witness of a run with none and follows it; accepted wherever a policy is spelled (`sysml -schedule`, `%schedule`, a conformance case's `schedule` pin) except the wire, where a request carries no file of the caller's and the spelling is `INVALID_ARGUMENT` | `runtime/replay.go` `ParseWitness`, `ParseInput`, `InputTaken`, `InputParseError`, `WitnessInputError`, `ParseChoices`, `ChoiceParseError`, `ReplayPolicy`, `SchedulePolicy.Replay`, `SchedulePolicy.Witness`; `runtime/action_executor.go` `ActionExecutor.fixWitnessInputs`, `Context.Unfollowed`, `ReplayError`, `ErrReplayRefused`, `replayRun.beginStep`, `replayRun.choose`; `runtime/scheduler.go` `ParseSchedulePolicy` (`replay:`), `ReplaySpelling`, `SchedulePolicyNames`; `grpc/service.go` `schedulePolicy` (the wire refusal); `cmd/sysml/usage.go` (the `-schedule` help) | `runtime/replay_input_test.go:TestParseInputReadsEverySpelling`, `:TestParseWitnessReadsInputsBeforeChoices`, `:TestReplayFixesWitnessInputs`; `smt/input_engine_test.go:TestEngineWitnessWithInputsReplaysThroughTheStart`, `:TestEngineWitnessWithoutInputsReplaysAsBefore`; `runtime/replay_test.go:TestParseChoicesRejectsWhatSpellsNoChoice`, `:TestParseChoicesStopsAtBlankLine`, `:TestParseReplayPolicy`, `repl/checker_test.go:TestReplayStepsAWitnessOfNoChoice`, `cmd/sysml/check_engine_test.go:TestEngineCheckWitnessOfNoChoiceReplays`, `:TestReplayFollowsActionWitnesses`, `:TestReplayFileReproducesTheExploredRun` (every witness `explore` tables replays to its run's trace), `:TestReplayFollowsStateWitnesses`, `:TestReplayRefusesAMoveNotEnabled`, `:TestReplayFallsBackToReverse`, `:TestReplayReadsAStepsOrderInEitherPlace`, `:TestReplayProbeLeavesTheWitnessInPlace`, `check_loop_replay_test.go:TestCheckWitnessesOfALoopingDoRoundReplay` (a `do` body looping through timed waits past the witness's last line, top-level), `:TestCheckWitnessesOfAnInlinePerformanceReplay` (an order recorded at the performer's step, resolved in the inner flow), `robustness_replay_test.go:TestRuntimeRobustnessReplay` (an inner order naming a token of no flow, or mixing two, refused; a witness ending before the run), `conformance/state_do_action_loop_timed_exit` + `.check.expected.json`; `cmd/sysml/spacecraft_showcase_test.go:TestEngineCheckWitnessesTheSpacecraftRaceAndReplaysEach` (every witness `-engine check` writes replays to its values); `grpc/schedule_test.go:TestAReplayScheduleIsInvalidArgument`; `smt/referee_test.go:TestRefereeCorpus`, `:TestRefereeInputs` (every solver witness, with its inputs, replayed under it) | ✅ As designed (an OpenSysML policy, like the others; no run under another policy changes) | | A choice point is reported wherever the executor had several enabled alternatives the library does not order, and nowhere else: several steppable tokens in one step (`token order`), several holding guards of one decision (`decision branch`), several tokens writing one feature in one step (`write order`), several enabled transitions out of one state for one event (`transition`), the next unit among the firings transitions in several regions selected for one event make (`region order`), the next entry among the regions a composite state, a fork or a history enters (`entry order`), and the next exit among the regions a state leaves (`exit order`). Each is one trace line `choice : (unordered; took )`, one informational diagnostic with code `choice-point` at the node, decision, feature or state that made it, and a count on the debugger's summary line; none is an error, and a run under a fixed policy reports the same set another fixed policy would have had to make on the linearization it took | `choice.go` `ChoiceKind` (`ChoiceTokenOrder`, `ChoiceDecisionBranch`, `ChoiceWriteOrder`, `ChoiceTransition`, `ChoiceRegionOrder`, `ChoiceEntryOrder`, `ChoiceExitOrder`), `ChoicePoint.String` (the trace line), `ChoicePoint.Describe`, `ChoicePoint.Diagnostic` (`SeverityInfo`, code `ChoiceDiagnosticCode`), `Context.noteChoice`, `Context.Notes`, `Context.Choices`, `ActionExecutor.Notes`/`StateExecutor.Notes`; `action_choice.go` `noteTokenOrder`, `noteDecisionBranches`, `stepWriteLedger.noteChoices`; `state_executor.go` `transitionChoice`, `chooseRegion`; `repl/trace.go` `Session.noteSummary` (`N choice points; %trace on to see them`, or the lines themselves under `%trace on`); `grpc/convert.go` `RunNoteDiagnosticsToProto` (the `diagnostics` of `ExecuteActionResponse`, `ExecuteStateResponse` and `RunAnalysisResponse`) | `choice_test.go:TestChoicePointRendering`, `:TestChoicesResetPerRun`, `:TestTransitionChoiceNamesStateAndEvent`, `:TestWriteConflictChoice`, `:TestSharedMessageAcceptIsAChoice`, `:TestAncestorPriorityIsNotAChoice`; `explore_test.go:TestExploreSiblingRegionOrder` (the `region order` kind); `conformance/action_choice_fork_token_order`, `action_choice_decision_overlapping_guards`, `action_choice_same_step_write_conflict`, `state_choice_transition_conflict`, `state_explore_region_order`, each `+ .trace.golden` carrying its `choice` line; `repl/choice_test.go:TestStepReportsChoicePoints`, `:TestStepChoiceSummaryWithTraceOn`, `:TestContinueReportsChoicePoints`, `:TestAdvanceReportsChoicePoints`; `grpc/choice_test.go:TestExecuteAction_ChoicePointDiagnostics`, `:TestExecuteState_ChoicePointDiagnostics`, `:TestRunAnalysis_ChoicePointDiagnostics`; `conformance_test.go:TestConformanceDiagnosticsGate` (an informational note is no failure of a conformance case) | ✅ Faithful (the ancestor-priority case between a substate's transition and its enclosing state's is ordered by SysML v2/KerML and is not reported — `state_choice_ancestor_priority_not_reported`, `state_choice_ancestor_outranked_not_reported`) | | A guard the executor reads only to report a choice — one after the branch or transition already taken — that fails to evaluate is no alternative and no error: a guard with no result is not true, so its succession is not selected, the run is unchanged, and the failure is an informational `guard-unevaluable` note, while the same failure at the guard the run does take still fails the run | `choice.go` `UnevaluableGuard`, `UnevaluableGuard.Diagnostic` (`SeverityInfo`, code `UnevaluableGuardCode`), `Context.noteUnevaluableGuard`, `Context.UnevaluableGuards`; `action_choice.go` `ActionExecutor.noteUnevaluableGuard`; `action_executor.go` `probeGuard` (a `beginProbe` preview that restores the budget, the trace, every effect and the object identities it took); `state_executor.go` `probeTransition`, `unevaluableTransition`; `state_route.go` `probeBranch` (a choice pseudostate's later branch); `state_change_trigger.go` `probeChangeGuard` | `conformance/action_choice_unevaluable_guard` + `.expected.json` + `.trace.golden`; `conformance/state_choice_unevaluable_transition` + `.expected.json` + `.trace.golden`; `conformance/state_choice_unevaluable_branch` + `.expected.json` + `.trace.golden`; `choice_test.go:TestLaterGuardErrorIsNotAChoiceNorAFailure`, `:TestLaterChoiceGuardErrorIsNotAChoiceNorAFailure`, `:TestFirstTransitionFailureStillFailsTheRun`, `:TestLaterGuardIsProbedWithoutCost`, `:TestLaterChangeGuardErrorIsNotAChoiceNorAFailure`, `:TestProbedGuardLeavesObjectIdentitiesUntouched`; `grpc/choice_test.go:TestExecuteAction_UnevaluableGuardDiagnostics`; `repl/choice_test.go:TestStepReportsUnevaluableGuards`; `robustness_test.go:decision_no_satisfied_guard` (the taken path still fails) | ✅ Faithful (SysML v2 §7.17.3 selects a succession whose guard is true; a guard that cannot be evaluated is not true, and reading it changed nothing) | | A conformance case that admits several outcomes states the whole set: `outcomes` lists every admissible outcome in full — never "anything" — and `admissible` cites the section of `behavior-semantic-oracle.md` that derives the set; a `.trace.order` beside it states the partial order the library does fix as `earlier < later` over trace labels. The harness checks the run's outcome is exactly one listed member, checks the trace against every order constraint, then explores the case and fails on a listed outcome no linearization reached, a reached outcome the list omits, and a hit budget — each naming the outcome and a witness choice sequence | `conformance_test.go` `ExpectedOutcome.Outcomes`/`Admissible`/`ExploreBudget`, `admissibleSchemaProblems` (an `outcomes` list needs two or more full outcomes, an `admissible` title the oracle has, and no single-outcome fields beside it), `matchOutcome`, `runConformanceCase`, `exploreConformanceCase`; `trace_test.go` `checkTraceOrder`, `parseOrderConstraints`, `orderViolations`; `testdata/conformance/README.md` (the schema) | `conformance_test.go:TestAdmissibleOutcomesSchema`, `:TestMatchOutcomeRequiresExactlyOne`, `:TestExecutionConformance` (`exploreConformanceCase` over every `outcomes` case), `:TestExecutionConformanceUnderPolicies`; `trace_test.go:TestTraceOrderViolationFails`, `:TestExecutionTrace`; `conformance/action_explore_three_writers` (`outcomes` of three, `.trace.order` of six constraints; explored in six runs), `action_choice_shared_message_accept`, `action_choice_fork_token_order` (one outcome, a `.trace.order` admitting the tokens either way), `state_explore_region_order` | ✅ Faithful (a case without `outcomes` is not explored by the harness, so a fixture pinning one linearization of an unobservable openness stays a one-outcome case; the oracle names which fixtures those are) | | Exploration is bounded and distinguishes setup failures from runtime outcomes: the budget is runs (1024 by default) and choice points per run (64), and a budget hit ends the search as `incomplete: budget hit after N runs`, naming `runs` before `depth`; a fresh-context or root-executor creation failure, or a pure structural start-check failure before root behavior runs, means no linearization ran and is returned without outcomes, while a failure during initial model behavior or later is an error outcome. Any runtime-error linearization makes the verdict fail, including an incomplete exploration; only an incomplete search without a witnessed failure is unresolved, and only a complete search without failures proves the outcome set. JSON and the wire include `failedLinearizations` / `failed_linearizations` only when nonzero; standing evidence names the failing count | `runtime/explore.go` `SetupError`, `Exploration.FailedLinearizations`; `runtime/context.go` root action/state setup classification; `runtime/explore_queue.go` `ExploreWith`, `exploreQueue.work`; `runtime/check.go` checker setup failure; `analysis/explore.go` proof strength; `analysis/standing.go` `Result.evidence`; `repl/explore.go` `explorationVerdict`; `cmd/sysml/report.go` `checkExploration`; `grpc/explore.go` `Service.explore` | `runtime/robustness_explore_failures_test.go:TestRuntimeRobustnessExploreFailures`; `analysis/explore_test.go`; `repl/explore_test.go:TestRunActionExploreSetupFailureRunsNoLinearization`, `:TestRunActionExplorePartialFailureFailsTheVerdict`; `cmd/sysml/run_test.go:TestExploreSetupFailureIsNotAnOutcome`, `:TestExplorePartialFailureFailsAndReportsTheFailedRun`, `:TestExploreInitialEntryFailureIsAnOutcome`; `grpc/explore_test.go:TestExploreSetupFailureIsReturnedInTheResponse`, `:TestExploreReportsAFailingRunAsAnOutcome` | ✅ Faithful (setup failures produce no outcome, while a witnessed runtime failure prevents a proof claim) | -| The set of outcomes is also reached by search rather than by enumeration: the explicit-state model checker takes the schedules of an *invocation* — the actions and state machines started on one clock, run to a horizon, and the machines of the objects they materialize — one *move* at a time — one token advancing one node, a body being one move (`stepToken`'s unit, coarser than the interpreter's statement), one event dispatched, one due `do` behavior stepped — enumerating the enabled moves of a state (each token able to act, a join whose tokens have all arrived, a paused token whose wait has ended, a parked `accept` whose message has arrived or whose routing fails under a readiness probe, one move per holding guard of a decision; a machine's dispatch, one per event the library leaves unordered at the instant, and its due do behaviors; among the executors due, the one drawn holds the turn until it has no move at the instant, as `runDue` draws it), snapshotting the run before each choice and restoring it to take the next, so every schedule the library admits is visited and no other. Two moves whose static *footprints* — the features a node's body reads (an outgoing succession's guard and a parked trigger's condition among them) and writes, the channels it sends on and accepts from, the joins and merges it reaches, computed once per node when the action is lowered; for a dispatch the guards and triggers of the transitions the event can select out of the active configuration, their effects and the activity of the states left and entered, for a do step its statements'; one executor's moves pairwise dependent — are independent commute, so only one order of each such pair is searched (persistent sets with a sleep set; a dynamic assignment target or send target depends on everything), and a state already visited — every executor in invocation order: the token multiset by node and frame path, frames root-first with values in the trace recorder's canonical form, a paused body's statement cursor, frames and wait, a machine's configuration, history, values, queue in dispatch order, deferred events, timers and do progress; objects by their materialization path rather than by `Instance.ID`, messages, the clock — is not searched again. What the search finds: a named constraint or requirement `false` at a stable state, a deadlock (`ErrActionDeadlock`, `ErrAcceptDeadlock`) or a typed error a body raises (an unbound parameter, a dangling succession, a division by zero, an `accept` whose `via` port does not resolve, each on the schedule that reaches it) is a *violation*; a feature ending with different final values on different schedules is *divergent*, one witness per value (the features named — `finalState` a machine's resting configuration, `.` and ` finalState` in a joint invocation — else every attribute of the behaviors and their performing object and every machine's `finalState`, an action without an object on its own); a machine resting where nothing wakes it is a complete schedule, not a deadlock, and a wait past the horizon is left unreached, the verdict reading `exhaustive up to t=D`; a bound reached — moves along one schedule (`depth`, 10 000), distinct states (`states`, 1 000 000), the plan's clock (`time`), an executor budget (`actionSteps`, `steps`, `elements`, …) — is named and the verdict is `no violation within bounds`, never `no violation, exhaustive`, the one proof, relative to the atomic move and the properties given. A witness is the `ChoiceTaken` sequence `explore` records for the same steps, written as its choice lines, a blank line and the run's trace, and is *replayed* through the scheduler seam (`replay:`, the policy that follows a witness's choices and refuses where the run departs) before it is reported: the replayed run must reach the state claimed with the same trace, else the witness is *not covered* with the disagreement as the reason. Where an exploration completes, the final states the check reaches are the outcomes it tabled. A body paused mid-statement — a token suspended at a breakpoint or on the clock, a do behavior waiting — is explicit state (`bodyRun`: statement cursor, block, loop and flow-node frames, the nested performance, a `bodyWait` descriptor) a snapshot of the same context captures and restores, so it is searched like any other; a portable `HeldImage` alone refuses it (`ErrSnapshotPausedBody`). Statement-level moves and following a signal to an object whose machine the invocation does not run are later stages'. The search is single-threaded — one executor state per stack frame, one visited set — so `Jobs` divides only the replay of witnesses | `lower/footprint.go` `Footprint`, `Footprints`, `Footprint.Dependent`, `footprintOf` (`Graph.Footprints` beside `Bodies`); `runtime/check.go` `Check`, `checker.search`, `checker.take`, `checker.enter`, `checker.visit`, `checker.stabilize`, `checker.properties`, `checker.final`, `CheckReport`, `CheckVerdict`, `CheckStopped`, `ExecutorBounds`; `runtime/check_reduce.go` `checker.persistent`, `checker.childSleep`, `checker.footprintOf`; `runtime/check_invocation.go` `Invocation`, `Starter`; `runtime/check_run.go` (the executors on one clock, the due order drawn, the horizon); `runtime/check_moves.go` `enabledMove`, `ActionExecutor.enabledMoves`, `ActionExecutor.makeMove`, `StateExecutor.dispatchMoves`, `checkPolicy`; `runtime/check_state.go` `canonicalState`, `stateSpeller`; `runtime/action_body_run.go` `bodyRun`, `bodyWait`; `lower/state_footprint.go` (transition and behavior footprints); `runtime/replay.go` `ParseChoices` (stops at the first blank line), `ReplayPolicy`, `Context.Unfollowed`, `ReplayError`; `runtime/check_replay.go` `Replay`, `Witness.String`, `ReplayDisagreement`; `runtime/action_executor.go` `acceptMatch` (a routing failure kept, not a deadlock); `analysis/check.go` `checkEngine` (below) | `runtime/check_test.go:TestCheckJoinWaitsForSlowestBranch` (`arrived = 3` on every schedule, nothing divergent), `:TestCheckForkBranchesWriteOneFeatureDiverge` (`x` over exactly `{1, 2}`, `leftRan`/`rightRan` agreed), `:TestCheckEvaluatesPropertiesAtCompletion`, `:TestCheckDivergenceOfNamedFeaturesOnly`, `:TestCheckWitnessesReplay`, `:TestCheckReplayDisagreesWithATamperedWitness`, `:TestCheckWitnessesAPerformedActionThroughItsPerformer`, `:TestCheckPropertyOfThePerformerIsWitnessed`, `:TestCheckReportsFailuresAsViolations` (the robustness failures as violations with a witness), `:TestCheckReportsAnUnresolvedViaPortAsTheRoutingError`, `:TestCheckMergeLoopHitsTheDepthBound` (`incomplete`, `depth` named, no hang, no exhaustiveness), `:TestCheckNamesEachExecutorBudget`, `:TestCheckStopsWhenCancelled`, `:TestCheckSettlesTimedBranchesOnTheClock`, `:TestCheckVisitedStatesCloseALoop`; `runtime/check_moves_test.go`; `runtime/check_reduce_test.go:TestCheckReductionIsSound` (reduced and unreduced final-state sets equal over `testdata/check/por_*.sysml`: shared write, guard read, trigger-condition read, send/accept pairing, join convergence, dynamic target), `:TestCheckReductionRatchet` (`testdata/check/reduction_expected.txt`, adjudicated on every movement); `runtime/check_corpus_test.go:TestCheckConformanceOracles` (every action, state and clock case with an admissible set against its `.check.expected.json`, reduced and unreduced — the eight cases whose default run pauses a body and `clock_action_state_due_together` among them), `:TestCheckAgreesWithExploreOverTheConformanceCorpus` (the referee), `:TestCheckWitnessesReplayOverTheConformanceCorpus`; `runtime/check_horizon_test.go` (a re-arming timer bounded by the horizon, a property up to it, an action's wait past it, a machine's failures as violations, the caller's deadline); `runtime/check_reduce_state_test.go` (dispatch and do-step footprints, one executor's moves as a unit); `runtime/action_body_run_test.go` (a body paused at a breakpoint, on the clock and inside a loop resumed, snapshotted and restored); `analysis/check_clock_test.go:TestExploreRefereesCheckOverBehaviorsOnOneClock`, `:TestChecksOfBehaviorsOnOneClockHaveWorkersOfTheirOwn`; `runtime/replay_test.go`; `lower/footprint_test.go`, `lower/state_footprint_test.go` | ✅ Faithful for actions and state machines on one clock (an exhaustive verdict is a proof relative to the atomic move — a body one move, a dispatch one move — and the properties named; statement-level moves and signals to machines off the clock are later stages') | +| The set of outcomes is also reached by search rather than by enumeration: the explicit-state model checker takes the schedules of an *invocation* — the actions and state machines started on one clock, run to a horizon, and the machines of the objects they materialize — one *move* at a time — one token advancing one node, a dividing leaf body's start shot or one of its statements being one move and any other body one move, one event dispatched, one due `do` behavior stepped — enumerating the enabled moves of a state (each token able to act, a join whose tokens have all arrived, a paused token whose wait has ended, a parked `accept` whose message has arrived or whose routing fails under a readiness probe, one move per holding guard of a decision; a machine's dispatch, one per event the library leaves unordered at the instant, and its due do behaviors; among the executors due, the one drawn holds the turn until it has no move at the instant, as `runDue` draws it), snapshotting the run before each choice and restoring it to take the next, so every schedule the library admits is visited and no other. Two moves whose static *footprints* — the features a node's body reads (an outgoing succession's guard and a parked trigger's condition among them) and writes, the channels it sends on and accepts from, the joins and merges it reaches, computed once per node when the action is lowered; for a dispatch the guards and triggers of the transitions the event can select out of the active configuration, their effects and the activity of the states left and entered, for a do step its statements'; one executor's moves pairwise dependent — are independent commute, so only one order of each such pair is searched (persistent sets with a sleep set; a dynamic assignment target or send target depends on everything), and a state already visited — every executor in invocation order: the token multiset by node and frame path, frames root-first with values in the trace recorder's canonical form, a paused body's statement cursor, frames and wait, a machine's configuration, history, values, queue in dispatch order, deferred events, timers and do progress; objects by their materialization path rather than by `Instance.ID`, messages, the clock — is not searched again. What the search finds: a named constraint or requirement `false` at a stable state, a deadlock (`ErrActionDeadlock`, `ErrAcceptDeadlock`) or a typed error a body raises (an unbound parameter, a dangling succession, a division by zero, an `accept` whose `via` port does not resolve, each on the schedule that reaches it) is a *violation*; a feature ending with different final values on different schedules is *divergent*, one witness per value (the features named — `finalState` a machine's resting configuration, `.` and ` finalState` in a joint invocation — else every attribute of the behaviors and their performing object and every machine's `finalState`, an action without an object on its own); a machine resting where nothing wakes it is a complete schedule, not a deadlock, and a wait past the horizon is left unreached, the verdict reading `exhaustive up to t=D`; a bound reached — moves along one schedule (`depth`, 10 000), distinct states (`states`, 1 000 000), the plan's clock (`time`), an executor budget (`actionSteps`, `steps`, `elements`, …) — is named and the verdict is `no violation within bounds`, never `no violation, exhaustive`, the one proof, relative to the atomic move and the properties given. A witness is the `ChoiceTaken` sequence `explore` records for the same steps, written as its choice lines, a blank line and the run's trace, and is *replayed* through the scheduler seam (`replay:`, the policy that follows a witness's choices and refuses where the run departs) before it is reported: the replayed run must reach the state claimed with the same trace, else the witness is *not covered* with the disagreement as the reason. Where an exploration completes, the final states the check reaches are the outcomes it tabled. A body paused mid-statement — a token suspended at a breakpoint or on the clock, a do behavior waiting — is explicit state (`bodyRun`: statement cursor, block, loop and flow-node frames, the nested performance, a `bodyWait` descriptor) a snapshot of the same context captures and restores, so it is searched like any other; a portable `HeldImage` alone refuses it (`ErrSnapshotPausedBody`). Statement-level moves and following a signal to an object whose machine the invocation does not run are later stages'. The search is single-threaded — one executor state per stack frame, one visited set — so `Jobs` divides only the replay of witnesses | `lower/footprint.go` `Footprint`, `Footprints`, `Footprint.Dependent`, `footprintOf` (`Graph.Footprints` beside `Bodies`); `runtime/check.go` `Check`, `checker.search`, `checker.take`, `checker.enter`, `checker.visit`, `checker.stabilize`, `checker.properties`, `checker.final`, `CheckReport`, `CheckVerdict`, `CheckStopped`, `ExecutorBounds`; `runtime/check_reduce.go` `checker.persistent`, `checker.childSleep`, `checker.footprintOf`; `runtime/check_invocation.go` `Invocation`, `Starter`; `runtime/check_run.go` (the executors on one clock, the due order drawn, the horizon); `runtime/check_moves.go` `enabledMove`, `ActionExecutor.enabledMoves`, `ActionExecutor.makeMove`, `StateExecutor.dispatchMoves`, `checkPolicy`; `runtime/check_state.go` `canonicalState`, `stateSpeller`; `runtime/action_body_run.go` `bodyRun`, `bodyWait`; `lower/state_footprint.go` (transition and behavior footprints); `runtime/replay.go` `ParseChoices` (stops at the first blank line), `ReplayPolicy`, `Context.Unfollowed`, `ReplayError`; `runtime/check_replay.go` `Replay`, `Witness.String`, `ReplayDisagreement`; `runtime/action_executor.go` `acceptMatch` (a routing failure kept, not a deadlock); `analysis/check.go` `checkEngine` (below) | `runtime/check_test.go:TestCheckJoinWaitsForSlowestBranch` (`arrived = 3` on every schedule, nothing divergent), `:TestCheckForkBranchesWriteOneFeatureDiverge` (`x` over exactly `{1, 2}`, `leftRan`/`rightRan` agreed), `:TestCheckEvaluatesPropertiesAtCompletion`, `:TestCheckDivergenceOfNamedFeaturesOnly`, `:TestCheckWitnessesReplay`, `:TestCheckReplayDisagreesWithATamperedWitness`, `:TestCheckWitnessesAPerformedActionThroughItsPerformer`, `:TestCheckPropertyOfThePerformerIsWitnessed`, `:TestCheckReportsFailuresAsViolations` (the robustness failures as violations with a witness), `:TestCheckReportsAnUnresolvedViaPortAsTheRoutingError`, `:TestCheckMergeLoopHitsTheDepthBound` (`incomplete`, `depth` named, no hang, no exhaustiveness), `:TestCheckNamesEachExecutorBudget`, `:TestCheckStopsWhenCancelled`, `:TestCheckSettlesTimedBranchesOnTheClock`, `:TestCheckVisitedStatesCloseALoop`; `runtime/check_moves_test.go`; `runtime/check_reduce_test.go:TestCheckReductionIsSound` (reduced and unreduced final-state sets equal over `testdata/check/por_*.sysml`: shared write, guard read, trigger-condition read, send/accept pairing, join convergence, dynamic target), `:TestCheckReductionRatchet` (`testdata/check/reduction_expected.txt`, adjudicated on every movement); `runtime/check_corpus_test.go:TestCheckConformanceOracles` (every action, state and clock case with an admissible set against its `.check.expected.json`, reduced and unreduced — the eight cases whose default run pauses a body and `clock_action_state_due_together` among them), `:TestCheckAgreesWithExploreOverTheConformanceCorpus` (the referee), `:TestCheckWitnessesReplayOverTheConformanceCorpus`; `runtime/check_horizon_test.go` (a re-arming timer bounded by the horizon, a property up to it, an action's wait past it, a machine's failures as violations, the caller's deadline); `runtime/check_reduce_state_test.go` (dispatch and do-step footprints, one executor's moves as a unit); `runtime/action_body_run_test.go` (a body paused at a breakpoint, on the clock and inside a loop resumed, snapshotted and restored); `analysis/check_clock_test.go:TestExploreRefereesCheckOverBehaviorsOnOneClock`, `:TestChecksOfBehaviorsOnOneClockHaveWorkersOfTheirOwn`; `runtime/replay_test.go`; `lower/footprint_test.go`, `lower/state_footprint_test.go` | ✅ Faithful for actions and state machines on one clock (an exhaustive verdict is a proof relative to the atomic move — a body one move, a dispatch one move — and the properties named; statement-level moves and signals to machines off the clock are later stages') | | The object an explored run performs a behavior on is one the run builds, never one the session holds: a performer, subject or object named to `explore`, `check`, `smt`, `sweep` or `all` is a *declaration* (a part/item usage or definition) each run instantiates, or a declaration-rooted *path* into what it holds (`Comms::pair.ground`, `Fleet::fleet.rovers[2]`) the run walks inside the object it made, the declaration being instantiated once per run however many behaviors name paths under it, so sibling parts share their assembly and its connectors carry their messages; a declaration `-instantiate` names is given to every run the same way, a machine named alone attaching to the run's one object exhibiting it; the path is checked against the declarations before any run starts (an unknown usage, an index on a single-valued usage, a step through a value) and what only a run can know (a part its recipe left unbuilt, an index past what it built) is that run's error; an id (`#2`, `#2.ground`) or a path from an object the session alone holds is refused as naming no recipe; the wire's `performer_symbol_id` and `subject_symbol_id` spell the same paths, and a witness the checker writes for a behavior on a nested object replays on it | `repl/explore.go` `freshRef` (the longest declaration prefix and the path past it), `checkFreshPath`, `freshPathError`, `planFresh`, `freshPlan.given`, `freshPlan.bind` (one root per declaration per run), `freshObjects.object` (the walk, through `objref.Walker`), `freshObjects.exhibitors`, `freshMachine`, `freshAction`, `freshExhibitorsError`, `UnplannedObjectError`, `ExploredObjectError`; `repl/session.go` `Session.given`, `givenRoots`; `repl/instantiate_report.go` (the CLI's `-instantiate` recorded as a given root); `repl/carryover.go` (a given root dropped with its declaration); `repl/meta.go` `ExhibitorsError.Fresh`; `objref/objref.go` `Ref`, `Segment`, `LooksLikePath`; `objref/walk.go` `Walker.Walk` (the one walk the prompt and the runs share, typed at each refusal); `runtime/context.go` `ExecuteActionPerformedBy`, `ExecuteStatePerformedBy`, `StateOutcomePerformedBy`; `runtime/check_invocation.go` `Invocation.Outcome` (the performers' attributes beside the behaviors'); `grpc/verify.go` `verifyContext.performer`, `subject`, `objectAt`; `grpc/service.go` (`CapabilityPerformer`; `performer_symbol_id` on `ExecuteActionRequest` and `ExecuteStateRequest`); `cmd/sysml/usage.go` (`-instantiate`, `-state`, `-action`); `client/opensysml/execute.go` `PerformedBy`; `client/python/opensysml/connection.py` (`performer=`) | `repl/explore_test.go:TestRunForExploresAMachineOnANestedObject`, `:TestRunForExploresAnActionOnANestedObject`, `:TestRunAnalysisExploresOnANestedSubject`, `:TestRunForExploresSiblingsOnOneRoot` (one root, two machines, the connector between them; the same table under one and four jobs), `:TestExploredRunsAreGivenTheObjectsInstantiated` (a machine alone attaches to the given object, a path reuses it, the prompt's `%instantiate` gives nothing, a given root outlives unrelated submissions and not its declaration's), `:TestExploredPathsAreCheckedAgainstTheDeclarations` (an id, an unknown usage, an index on one value, a step through a value: refused before any run; a part left unbuilt: the run's error); `cmd/sysml/explore_nested_test.go:TestExploreRunsAMachineOnANestedObject` (a connector race tables two outcomes, its no-race variant one, byte for byte under `-jobs 1` and `-jobs 4`), `:TestExploreRunsSiblingsOnOneAssembly`, `:TestExploreRefusesPathsItCannotPlan`, `:TestEngineCheckWitnessesANestedObjectsDivergence` (the witness written, replayed under `-schedule replay:`, and composed under `-engine all`); `runtime/check_test.go:TestCheckWitnessesOfSeveralBehaviorsReplay`, `runtime/replay_test.go:TestReplayFollowsDoActionWitnesses`, `:TestReplayFollowsSiblingDoActionWitnesses`; `grpc/explore_test.go:TestPerformOnANestedObjectOverTheWire`, `grpc/analysis_test.go:TestRunAnalysisOnANestedSubject`, `grpc/capability_test.go` (`performer` advertised); `client/opensysml/performer_test.go`; `client/python/tests/test_performer.py` | ✅ Faithful (the runs' objects are built by the same instantiation and walked by the same `objref.Walker` as the prompt's, so a path means one thing at the prompt and in a run; the session's objects stay out of every run by construction, the plan being made under the session's lock and the objects inside each run) | | Action execution nodes | `action_executor.go:723` stepActionExecutionNode | `action_control_flow.sysml` | ✅ Faithful | | Nested action invocation (`action call : Callee;`, `action call = Callee(3, 4);`, `action call = Callee(a = 3);`, `perform action call : Callee;`) | `runtime/action_frame.go` `bindArguments`, `performInvocation`, `checkInputsBound`; `runtime/state_statements.go` `stateStmtHost.performNode`; `runtime/invoke_action.go` `invocationArguments`, `bindArgumentList`, `Context.actionParametersOf` (over `semantics.Model.BehaviorParametersOf`, so inherited and redefined parameters keep their effective order); `action_executor.go` `stepNestedAction` | `invoke_action_test.go:TestInvokeActionPassesParametersBothWays`, `:TestInvokeActionBindsPositionalArguments`, `:TestInvokeActionBindsNamedArguments`, `:TestInvokeActionRejectsBadArguments`; `conformance/action_node_invocation_positional`, `action_node_invocation_named`, `action_node_typed_body_inputs`, `action_node_inherited_parameters`, `action_node_invocation_empty`, `action_inherited_typed_node_scope`, `action_node_arguments_before_defaults`, `state_block_flow_node_arguments_before_defaults` | ✅ Faithful (arguments bind the callee's inputs by the callee's parameter order and names, never by what the caller happens to name alike; they bind the node's pins before the defaults it declares are evaluated, so a default an argument replaces is never evaluated and one the node keeps reads the argument's value, in an action's flow and in a state's body alike; the callee is resolved where the node was declared, so a node a derived action inherits still finds a callee visible only to the general action; a surplus, missing, unknown or repeated argument is `ErrActionArity`, `ErrUnboundParameter`, `ErrUnknownParameter` or `ErrDuplicateArgument` before the callee runs — a surplus one is also reported statically by `passes/typecheck_expr.go`, so no conformance fixture states it. The row above states the compatibility by-name fallback a bare usage keeps) | @@ -864,7 +868,7 @@ nothing above. | An entry, do or exit behavior written as an inline action body (`entry action { … }`, `do action named { … }`, `exit action { … }`) executes the statements it states, locals and loops among them, in any nesting of composite states; an empty body is a behavior that does nothing. A braced block without the keyword (`entry { … }`, `do { … }`, `exit { … }`, a transition's `do { … }`; SysML.xtext `StateActionUsage`/`EffectBehaviorUsage`: `PerformedActionUsage ActionBody`) is one anonymous action usage with that body — the same tree as `entry action { … }`, the keyword left empty so the spelling is kept — so its declarations are local to the block and shadow the state's, and the block is one behavior | `parser/behavior.go` `parseBracedActionUsage` (from `parseStateSubactionBlock`, `parseTransitionEffect`); `lower/state_behavior.go` `LowerBehaviors`/`lowerStateBehavior` (the body is lowered to a `Block`, its locals in the block's own frame), `lower/state_graph.go` `StateGraph.Behaviors`; `runtime/state_statements.go` `executeBehavior`/`stateStmtHost` | `tests/parser/testdata/parse/state_anonymous_action_body.golden`, `state_braced_block_one_action.golden` (the braced and the `action` spellings, the same structure), `state_anonymous_action_body.sysml` + trace golden (entry/do/exit ordering, nesting, empty bodies), `state_braced_block_local_attribute.sysml` + trace golden (a `k` declared in each of the four blocks shadows the machine's, which stays untouched), `state_transition_braced_do_then_accept.sysml` + trace golden (a transition's block of three statements, an accept-triggered follow-on with its own), `robustness_test.go:testEmptyAnonymousActionBody`, `:testNonTerminatingAnonymousDoBody` (`ErrStepLimitExceeded`), `robustness_braced_block_test.go` (empty blocks, a block of one `terminate`, an unbound name in a block, a block-local attribute reached from outside refused as unresolved) | ✅ Faithful | | An inline entry, do or exit body that states a token flow of its own — successions (`first start; then action a; then done;`, `first a then b;`), forks, joins, decisions with guards, and action nodes with a flow of their own — runs that flow as a standalone action's body does: `then done` completes the behavior, the attributes the body declares are the performance's own (defaults evaluated where written, a node's assignment read by the next, shadowing the machine's without writing it), a dangling or unstartable succession, or the body or a node of it declaring `return`, is a typed error before any node runs; a flow no `first` starts begins at the nodes no succession leads to (`do action poll { action wait accept after 3 [s]; then action count assign ticks := ticks + 1; }`), several such nodes starting together as unordered subactions, and a cycle over every node leaving no start, reported, in the flow a nested node states as in the body's own | `lower/state_behavior.go` `lowerBehaviorBody` (a body stating a flow, `statesOwnFlow`, is lowered through `ToActionGraph` to a stated `Block`, `lower/case_body.go` `StartFlow`/`CaseFlowStart` giving it its declaration-order start; one stating none stays a statement block), `lower/action_subflow.go` `lowerActionNode` (a nested node's flow is started the same way); `runtime/state_statements.go` `stateStmtHost.runFlow`/`runOwnFlow`/`setFeature` (the block runs through the behavior's own `ActionExecutor`: `checkResultParameters`, `declareRootFeatures`, `declareAcceptPayloads`, `initializeAttributes`, `runSubflow`; `noFlowStart` names the unpreceded nodes or the cycle) | `tests/parser/testdata/parse/state_action_body_successions.golden`, `lower/state_behavior_test.go`, `state_do_action_successions_first_start.sysml` + trace golden, `state_do_action_successions_named_first.sysml`, `state_do_action_fork_join_decision.sysml`, `state_do_action_body_attributes.sysml`, `state_entry_exit_action_successions.sysml` + trace golden, `robustness_test.go:testStateBlockNodeOwnFlowRuns`, `:testStateDoBodyNodeReturnParameter`, `:testStateDoBodyReturnParameter`, `:testStateDoBodyDanglingSuccession`, `:testStateDoBodyFirstThenUndefined`, `:testStateDoBodyFlowWithoutStart`, `:testStateDoBodyNestedNodeDanglingSuccession`, `:testStateEntryBodyDanglingSuccession`, `:testStateDoBodyFlowThatNeverEnds`, `:testStateDoBodyStartsAtItsUnprecededStep`, `:testActionFlowStartsAtItsUnprecededStep`, `:testActionFlowCycleWithoutStart`, `:testStateDoBodyNestedNodeStartsAtItsUnprecededStep`, `:testActionNestedNodeStartsAtItsUnprecededStep`, `robustness_unordered_subactions_test.go:TestRuntimeRobustnessUnorderedSubactions/two_unpreceded_steps_both_start`, `/nested_node_two_unpreceded_steps_both_start`, `/state_do_body_two_unpreceded_steps_both_start`, `lower/state_behavior_test.go:TestStateBehaviorBodyStartsAtItsOneUnprecededStep`, `:TestStateBehaviorBodyWithAmbiguousStartKeepsNoInitial`, `lower/action_subflow_test.go:TestActionNodeSubflowStartsAtItsOneUnprecededStep`, `:TestActionNodeSubflowWithoutOneStartKeepsNoInitial`, `lower/action_succession_test.go:TestStartFlow`, conformance `state_do_action_declaration_order.sysml` + trace golden | ✅ Faithful | | 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 — 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); `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` in two regions' inline bodies, the four interleavings of `state_concurrent_do`); `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`) | ✅ 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) | | 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 | diff --git a/examples/self-model/README.md b/examples/self-model/README.md index 41f1a0f926..64e456921d 100644 --- a/examples/self-model/README.md +++ b/examples/self-model/README.md @@ -21,7 +21,7 @@ The eight files: | --- | --- | | [pipeline.sysml](pipeline.sysml) | `OpenSysMLArtifacts` — what travels between stages (source text, tokens, tree, spans, symbol index, the library snapshot, side tables, diagnostics, IR graphs, traces, RDF, document trees), the ports and channels it travels over, and the layer metadata the filtered views select on. `OpenSysMLPipeline` — the fourteen stages from `internal/syntax/source` to the analysis framework, each naming the Go package that implements it; `PassRegistry`, holding all sixty-six registered validation passes with the tier each runs at and whether it gates itself per element; the standard library with the embedded snapshot its index is decoded from, the codec and generator units behind it and the variable that overrides it; the runtime's eight independent budgets with their defaults and environment variables, and the evaluator with the compiled tier beside it and the variable that switches that tier off; the runtime's split of model-derived from run-derived state, with the snapshot store and the exploration queue that split makes possible; `AnalysisFramework` — the seven question kinds and three freedoms, the five-step evidence scale and ten claims, the per-owner engine registry, the dispatcher with its three selections, the eight-field plan budget with the `OPENSYSML_JOBS` variable, the worker fleet, and the seven engines this build registers (`check`, `explore`, `run`, `smt`, `solve`, `sweep`, `tool:fmi`), each declaring what it answers, what bounds it, and the strongest evidence it can produce; and `AnalysisPipeline`, which wires the stages together and puts the framework's questions to the runtime and the solver | | [behavior.sysml](behavior.sysml) | one document analysed end to end (`AnalyzeDocument`, whose four decision nodes are the tier gates), the editor's edit-then-sweep path (`ServeEdit`), the library loaded once per process (`LoadLibrary`, whose two decision nodes are the digest and checksum checks that decide between the snapshot and the files), one calc invoked (`InvokeCalc`, whose three decision nodes — tracing, body, arguments — send it to the compiled tier or to the evaluator), one feature read (`ReadFeatureValue`, whose ten decision nodes are the cases a feature can be in — undeclared, bound, already held, a variation, a `default` yielding to contributions, a stated value, an abstract or optional connector or composite holding only contributions, a connector, a composite, and a plain feature with nothing to hold, empty when optional and uninitialized when required — each ending in the value admitted, held or refused), and five state machines: the validation tier ladder, the runtime's five tiers, token flow over the action graph with its deadlock and budget exits, run-to-completion event dispatch with deferral, and the eight ways a run ends early when a budget is exhausted; then the analysis framework: one question answered (`AnswerQuestion`, whose decision nodes are the selection — `auto` by authority, `all` by name, a name alone, each over the engines declaring the question's kind — the coverage check, the fault and the standing that decides whether the plan goes on to the next candidate, with every engine consulted — answering, refusing or faulting — a step of the plan), a behavior's outcomes explored over a fleet of workers (`ExploreOutcomes`, whose decision nodes after each run are the choices the run meets on its way down, each taken at its first alternative — within the depth bound each leaves its second alternative on the queue, beyond it the depth bound is hit — the next alternative of the choice its prefix ended at, which it leaves while one is untried, the run budget, which caps the queue at the runs left and drops the rest, hitting the runs bound, and the head of the queue in plan order, every departure from the first run before any second one — so that the queue always drains, proves when neither bound was hit and observes otherwise), and three more state machines — the evidence ladder from not covered to proved, a worker's life in a plan, and a snapshot as a mark between steps with the two asks it refuses | -| [execution.sysml](execution.sysml) | the runtime's instance layer, deep enough to draw: the effective feature a type's schema is made of, the object and the feature value it holds, and the six units the layer is — the schema built once per type (`FeaturesOf`), the allocator that claims an identity and folds only constant defaults (`materialize`), the lazy reader that makes a value on its first read (`GetFeatureValue`), the binding propagator that reads a bound feature from its far end, the admission every held value passes (multiplicity, type, uniqueness, the preferred unit), and the dependency tracker that sends a derived value back to be derived again when a value it read is written — with the value flows between them; around it, the run context, the scheduler with its eleven kinds of choice and its policy spellings, and `ExecuteAction`, one action executed as an interaction: the surface's request, the declaration lowered to an action graph, the executor stepped with every choice put to the scheduler, each guard evaluated, each feature read lazily and each write invalidating what depended on it, then the trace and the results returned | +| [execution.sysml](execution.sysml) | the runtime's instance layer, deep enough to draw: the effective feature a type's schema is made of, the object and the feature value it holds, and the six units the layer is — the schema built once per type (`FeaturesOf`), the allocator that claims an identity and folds only constant defaults (`materialize`), the lazy reader that makes a value on its first read (`GetFeatureValue`), the binding propagator that reads a bound feature from its far end, the admission every held value passes (multiplicity, type, uniqueness, the preferred unit), and the dependency tracker that sends a derived value back to be derived again when a value it read is written — with the value flows between them; around it, the run context, the scheduler with its thirteen kinds of choice and its policy spellings, and `ExecuteAction`, one action executed as an interaction: the surface's request, the declaration lowered to an action graph, the executor stepped with every choice put to the scheduler, each guard evaluated, each feature read lazily and each write invalidating what depended on it, then the trace and the results returned | | [surfaces.sysml](surfaces.sysml) | the five interfaces over one pipeline (REPL, LSP with every capability it advertises, gRPC/Connect service, stdio service, CLI), the three that ask questions of the analysis framework with the engine selector, jobs setting and engine listing each exposes (`%engine`/`%jobs`/`%engines`, the `engine` request field, `OPENSYSML_JOBS` and `ListEngines`, `-engine`/`-jobs`/`-engines`), the protobuf schema they are generated from with its twenty-three RPCs, the five generated clients, the Julia and MATLAB clients that speak Connect-JSON by the wire contract instead, the VS Code extension and the Cameo and SysON integrations over the Java client, the editor pipeline (highlighting, quick fixes, suggestions, source edits, formatting, provenance), the view engine with the eight rendering kinds it recognises and the six it produces, the document path from a query in the model through the plan, the backend-agnostic tree and the three backends to Markdown, HTML or PDF (`RenderDocument` branches on the form, and on whether the PDF converters are installed), the exporter and its accepted format names, the SysML v1 migrator and the calc code generator, the twelve conformance oracles with their committed baselines and the pin, errata and census infrastructure behind them, and `Toolchain`, which holds all of it | | [identity.sysml](identity.sysml) | the element-identity path: the `IdentityMetadata` library the ids are carried by, the encoder that derives an id from a qualified name, the side table that computes each element's effective id, the constraint-tier pass that checks the generated id space, the RDF writer and reader that carry identity through a graph, the Flexo harness that measures a live round trip, the repository sync that diffs a local model against its repository by effective id (`SyncModel`: scope, state, diff, conflicts, minting, write-back) with the `sysml -sync-*` flags that drive it, and the one phase of the [design record](../../docs/project/element-identity-annotations.md) not built — the notation extension filed with OMG | | [quality.sysml](quality.sysml) | fourteen architecture invariants as `requirement def`s bound to the modelled parts, the test runs that verify them as `verification def`s, the contributor's use case, and the allocation of every logical unit onto its directory in the source tree | diff --git a/examples/self-model/execution.sysml b/examples/self-model/execution.sysml index a9418387fb..a119f62f01 100644 --- a/examples/self-model/execution.sysml +++ b/examples/self-model/execution.sysml @@ -166,7 +166,7 @@ package OpenSysMLExecution { // run records a choice point, which a replay or an exploration takes differently. part def Scheduler :> CodeUnit { attribute :>> goPackage = "internal/exec/runtime"; - attribute choiceKindCount : Integer = 11; + attribute choiceKindCount : Integer = 13; attribute defaultPolicy : String = "reverse"; attribute policySpellings : String = "declared, reverse, seed:, explore[:runs=,depth=], replay:"; attribute recordsChoicePoints : Boolean = true; diff --git a/internal/exec/runtime/action_body_run.go b/internal/exec/runtime/action_body_run.go index f3e7a7d65d..650460f023 100644 --- a/internal/exec/runtime/action_body_run.go +++ b/internal/exec/runtime/action_body_run.go @@ -33,6 +33,22 @@ type bodyWork interface { spell(*stateSpeller) string } +func (ctx *Context) enclosingExecutorStep() int { + if ctx.body == nil { + return 0 + } + switch work := ctx.body.work.(type) { + case *usageWork: + return work.exec.stepCount + 1 + case *statementWork: + return work.exec.stepCount + 1 + case *executionWork: + return work.exec.stepCount + 1 + default: + return 0 + } +} + // bodyFrame is where one level of a body's work paused; abandon ends what it // holds open, clone copies it as it stands, for a snapshot to restore it to, and // spell writes it into a state's canonical form. @@ -67,9 +83,70 @@ type bodyRun struct { // yields has the run pause at the statement boundary after the statement, // loop iteration or flow step it performed since resumed, which performed marks. yields, performed bool + // guards has it yield between an `if`'s guard and its branch as well. + guards bool + // nodesYield makes a loop or `if` node of the flow the run states yield as a statement of its body does. + nodesYield bool + // draws has a seeded run draw whether to yield at each such boundary, by the + // token the run is for and how many boundaries it drew at before. + draws bool + token int64 + boundary uint64 // steps has the run pause after each token move of the flows and actions it - // drives where a step is one move, its machine going on between the moves. - steps bool + // 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 + // 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 + // lists are the unordered statement lists running, outermost first. + lists []*listLevel +} + +// listLevel is an unordered statement list running in a body: moved is whether its +// statement running made a move since it started or went on. +type listLevel struct { + frame *stmtListFrame + order *lower.StatementOrder + moved bool +} + +// enterList notes f running in the body on the stack; nil where none is. +func (ctx *Context) enterList(f *stmtListFrame, order *lower.StatementOrder) *listLevel { + if ctx.body == nil { + return nil + } + level := &listLevel{frame: f, order: order} + ctx.body.lists = append(ctx.body.lists, level) + return level +} + +// leaveList notes the innermost list entered, level, done running. +func (ctx *Context) leaveList(level *listLevel) { + if level == nil { + return + } + lists := ctx.body.lists + ctx.body.lists = lists[:len(lists)-1] +} + +// switchStrand pauses the body on the stack back to the innermost unordered list +// whose statement running has moved and does not commute with one it may set +// aside for; nil, going on, where none is. +func (ctx *Context) switchStrand() error { + run := ctx.body + if run == nil { + return nil + } + for k := len(run.lists) - 1; k >= 0; k-- { + l := run.lists[k] + f := l.frame + if f.i < 0 || !l.moved || len(l.order.Rivals(f.i, f.done, f.divided)) == 0 { + continue + } + return ctx.pauseBody(bodyPause{yielded: true, strand: f}) + } + return nil } // bodyPause is why a body run paused: at the breakpoint, on a wait, yielded at a @@ -80,6 +157,9 @@ type bodyPause struct { wait bodyWait yielded bool tokenStep bool + // strand is the unordered statement list the pause unwinds to, which sets the + // statement it unwound from aside rather than pausing the body; nil for none. + strand *stmtListFrame } // bodyWait is the wait a body's run paused on: of the action it performs (held), @@ -205,21 +285,46 @@ func (run *bodyRun) end(ctx *Context) { // endPerformed ends perf where a body statement of the paused run was performing it, // abandoning the levels within it: the run resumed goes on past the node as completed. func (run *bodyRun) endPerformed(ctx *Context, perf *actionFrame) bool { - for i, f := range run.cursor { - pf, ok := f.(*performFrame) - if !ok || pf.perf != perf { - continue - } - for _, inner := range run.cursor[:i] { - inner.abandon(ctx) - } - run.cursor = run.cursor[i:] + rest, pf, inStrand := endPerformedIn(ctx, run.cursor, perf) + if pf == nil { + return false + } + if !inStrand { + run.cursor = rest run.traceLevels = pf.levels run.paused = bodyPause{} - pf.ended = true - return true } - return false + return true +} + +// endPerformedIn finds the frame performing perf in cursor, or in a statement an +// unordered list in it set aside, abandoning the levels within it; rest is what of +// cursor is left, unchanged where the frame was in a statement set aside. +func endPerformedIn(ctx *Context, cursor []bodyFrame, perf *actionFrame) (rest []bodyFrame, ended *performFrame, inStrand bool) { + for i, f := range cursor { + switch f := f.(type) { + case *performFrame: + if f.perf != perf { + continue + } + for _, inner := range cursor[:i] { + inner.abandon(ctx) + } + f.ended = true + return cursor[i:], f, false + case *stmtListFrame: + for _, s := range f.strands { + if s == nil { + continue + } + if left, pf, _ := endPerformedIn(ctx, s.cursor, perf); pf != nil { + s.cursor, s.levels, s.paused = left, pf.levels-f.levels, bodyPause{} + return cursor, pf, true + } + } + } + } + return cursor, nil, false } // bodyLevels is the trace nesting the body on the stack holds open at this point @@ -325,6 +430,9 @@ func (w *usageWork) perform() error { w.phase = usageBody default: w.phase = usageBody + if lower.ReadsAtStart(w.graph, w.usage) { + e.ctx.bodyPerformed() + } } } if w.phase == usageBody { @@ -443,16 +551,85 @@ func (e *ActionExecutor) workToken(id int64) (int, error) { return idx, nil } +// bodyDivides reports whether another performance may interleave inside work's +// body with an effect on an outcome, so a run going one move at a time yields in it; +// open where moves outside its flow may interleave too. +func (e *ActionExecutor) bodyDivides(work bodyWork, open bool) bool { + var graph *lower.ActionGraph + var node ast.Node + switch w := work.(type) { + case *usageWork: + if w.performs { + return false + } + graph, node = w.graph, w.usage + case *statementWork: + graph, node = w.frame.graph, w.node + default: + return false + } + if graph == nil { + return false + } + key := bodyDivision{node: node, open: open} + divides, known := e.divides[key] + if !known { + divides = lower.BodyDivides(graph, node) || open && lower.BodySharesMoves(graph, node) + if e.divides == nil { + e.divides = make(map[bodyDivision]bool) + } + e.divides[key] = divides + } + return divides +} + +// bodyDivision keys the cache of bodyDivides. +type bodyDivision struct { + node ast.Node + open bool +} + // runBody starts work for the token at tokenIdx and drives it to its first pause or end. +// A run one move at a time with another move open goes one move at a time inside +// the work too, through the flows and actions it performs (stepsTokens). func (e *ActionExecutor) runBody(tokenIdx int, work bodyWork) error { run := &bodyRun{work: work, awaitsMessages: true} if outer := e.ctx.body; outer != nil { - run.awaitsMessages, run.steps = outer.awaitsMessages, outer.steps + run.awaitsMessages, run.steps, run.shared = outer.awaitsMessages, outer.steps, outer.shared + run.stepDraws, run.token = outer.stepDraws, outer.token + } + open := run.steps + scheduling := e.ctx.scheduling() + if scheduling.oneMove() && len(e.tokens) > 1 && !run.steps { + run.steps, run.shared = true, true + } + if _, draws := scheduling.bodyYields(len(e.tokens) > 1); draws && !run.stepDraws { + run.stepDraws, run.token = true, e.tokens[tokenIdx].ID + } + if yields, draws := scheduling.bodyYields(len(e.tokens) > 1 || open); yields && (open || !e.tokens[tokenIdx].drivenByBody()) && e.bodyDivides(work, open) { + run.yields, run.draws, run.token, run.guards = yields, draws, e.tokens[tokenIdx].ID, true + } + if outer := e.ctx.body; outer != nil && outer.nodesYield && yieldsAsStatement(work) { + run.yields, run.nodesYield = true, true + run.guards = run.guards || outer.guards } e.tokens[tokenIdx].body = run return e.resumeBody(tokenIdx) } +// yieldsAsStatement reports work that is a loop or `if` written as a node of a flow. +func yieldsAsStatement(work bodyWork) bool { + w, ok := work.(*statementWork) + if !ok { + return false + } + switch w.node.(type) { + case *ast.WhileLoopActionNode, *ast.IfActionNode: + return true + } + return false +} + // Release ends the run for good: the work of every token a breakpoint left // paused is ended, so an executor abandoned mid-run holds no suspended run, the // clock drives it no further, and a later Step or RunToCompletion returns @@ -511,7 +688,12 @@ func (e *ActionExecutor) resumeBody(tokenIdx int) error { if pause, paused := run.resume(e.ctx); paused { e.pauses++ run.pausedAt = e.pauses - if !pause.onWait && !pause.tokenStep { + switch { + case pause.yielded || pause.tokenStep: + if i := e.tokenIndex(id); i >= 0 { + e.tokens[i].moved = e.sweep + } + case !pause.onWait: e.pausedAt = pause.breakpoint e.state = StateSuspended } @@ -546,9 +728,18 @@ func (ctx *Context) pauseBody(pause bodyPause) error { // yieldBody pauses the body on the stack before its next statement where its run // goes one at a time and has performed one since resumed; nil, going on, else. func (ctx *Context) yieldBody() error { + if err := ctx.switchStrand(); err != nil { + return err + } if ctx.body == nil || !ctx.body.yields || !ctx.body.performed { return nil } + if ctx.body.draws { + ctx.body.boundary++ + if !ctx.scheduling().drawYield(ctx.body.token, ctx.body.boundary) { + return nil + } + } return ctx.pauseBody(bodyPause{yielded: true}) } @@ -557,6 +748,14 @@ func (ctx *Context) yieldBody() error { func (ctx *Context) bodyPerformed() { if ctx.body != nil { ctx.body.performed = true + ctx.body.listsMoved() + } +} + +// listsMoved notes a move made by the statement each unordered list running runs. +func (run *bodyRun) listsMoved() { + for _, l := range run.lists { + l.moved = true } } @@ -565,15 +764,62 @@ func (ctx *Context) stepsTokens() bool { return ctx.body != nil && ctx.body.steps } -// tokenStepBody pauses the body on the stack after one token move where its run -// goes one move at a time; nil, going on, else. -func (ctx *Context) tokenStepBody() error { - if !ctx.stepsTokens() { +// guardPerformed notes an `if`'s guard read by the body on the stack, after which a +// run yielding between a guard and its branch yields. +func (ctx *Context) guardPerformed() { + if ctx.body == nil { + return + } + ctx.body.listsMoved() + if ctx.body.guards { + ctx.body.performed = true + } +} + +// tokenStepBody pauses the body on the stack after one token move of graph's flow +// where its run goes one move at a time there, or a seeded draw says so; nil, going on, else. +func (ctx *Context) tokenStepBody(graph *lower.ActionGraph) error { + switch { + case ctx.stepsTokens(): + if ctx.body.shared && !ctx.flowSharesMoves(graph) { + return nil + } + case ctx.drawsTokenSteps(): + if !ctx.flowSharesMoves(graph) { + return nil + } + ctx.body.boundary++ + if !ctx.scheduling().drawYield(ctx.body.token, ctx.body.boundary) { + return nil + } + default: return nil } return ctx.pauseBody(bodyPause{tokenStep: true}) } +// drawsTokenSteps reports whether the body on the stack is a seeded run drawing +// whether to pause after the moves of the callees it performs. +func (ctx *Context) drawsTokenSteps() bool { + return ctx.body != nil && ctx.body.stepDraws +} + +// flowSharesMoves caches lower.FlowSharesMoves by graph. +func (ctx *Context) flowSharesMoves(graph *lower.ActionGraph) bool { + if graph == nil { + return false + } + shares, known := ctx.flowShares[graph] + if !known { + shares = lower.FlowSharesMoves(graph) + if ctx.flowShares == nil { + ctx.flowShares = make(map[*lower.ActionGraph]bool) + } + ctx.flowShares[graph] = shares + } + return shares +} + // yieldedHere reports the frame just popped as the one the body yielded in: its // next statement begins afresh there, where a frame paused inside one resumes it. func (ctx *Context) yieldedHere() bool { diff --git a/internal/exec/runtime/action_executor.go b/internal/exec/runtime/action_executor.go index c175855ac3..3643735fd8 100644 --- a/internal/exec/runtime/action_executor.go +++ b/internal/exec/runtime/action_executor.go @@ -47,6 +47,8 @@ type ActionExecutor struct { occurrence *Instance graph *lower.ActionGraph // Execution IR stepCounts map[stepMultiplicityKey]stepMultiplicityResult + // divides caches, by node, whether a body's moves may interleave with another's (lower.BodyDivides). + divides map[bodyDivision]bool // features are the attributes and parameters the performance holds: those the // graph declares, then the inherited ones none of them redefines. features []lower.Attribute @@ -390,7 +392,10 @@ func (e *ActionExecutor) Step() error { if acted { e.moved = true } - progressMade := e.tokensProgressed(tokenCountBefore, tokenLocationsBefore) + // A one-move step ends at its move with the other tokens untried, so the move is + // progress even when the work it resumed parks again where it was. + progressMade := e.tokensProgressed(tokenCountBefore, tokenLocationsBefore) || + acted && e.ctx.scheduling().oneMove() if err != nil { e.endPausedBodies() return err @@ -743,10 +748,10 @@ func (e *ActionExecutor) run(atCurrentTime bool) error { // pauseAfterMove pauses the body performing this action after one token move where // its run goes one move at a time and another move is open now; nil else. func (e *ActionExecutor) pauseAfterMove() error { - if !e.ctx.stepsTokens() || e.state != StateRunning || !e.canAct(nil) { + if !e.ctx.stepsTokens() && !e.ctx.drawsTokenSteps() || e.state != StateRunning || !e.canAct(nil) { return nil } - return e.ctx.tokenStepBody() + return e.ctx.tokenStepBody(e.graph) } // StepToBreakpoint is Step with the breakpoints a run stops at: a token sitting @@ -858,6 +863,7 @@ func (e *ActionExecutor) canProceed(perf *actionFrame) bool { // changeWaitHolds reports a token of perf's flow (the action's for nil) parked at // an accept whose condition holds now; one the step cannot evaluate counts, so the step reports it. func (e *ActionExecutor) changeWaitHolds(perf *actionFrame) bool { + defer e.ctx.beginProbe()() return e.changeWaitHoldsIn(perf, make(map[waitTarget]bool)) } @@ -1259,6 +1265,11 @@ func (e *ActionExecutor) NodeNames() []string { return append(names, e.subflowNodeNames(e.graph)...) } +// readsAtStart reports whether the performance evaluated an initial value at its start shot. +func (e *ActionExecutor) readsAtStart() bool { + return slices.ContainsFunc(e.features, func(attr lower.Attribute) bool { return attr.Value != nil }) +} + // initializeAttributes fills the features no supplied input holds: from the occurrence's // slots, else the declared defaults in order, each evaluated where it was declared. func (e *ActionExecutor) initializeAttributes() error { @@ -1727,10 +1738,15 @@ func (e *ActionExecutor) stepTokenAt(tokenIdx int) error { if node.Kind == ast.UsageAction || lower.IsCaseNode(node) { return e.stepNestedAction(tokenIdx) } - if node.Kind == ast.UsageConstraint { + if e.tokenGraph(tokenIdx).StatementRuns[node] || node.Kind == ast.UsageConstraint { return e.stepStatementNode(tokenIdx) } return fmt.Errorf("unsupported usage kind in action: %v", node.Kind) + case *ast.PerformActionNode: + if e.tokenGraph(tokenIdx).UnstatedCaseFlow { + return e.stepStatementNode(tokenIdx) + } + return fmt.Errorf("unsupported node type: %T", node) case *ast.WhileLoopActionNode, *ast.IfActionNode, *ast.AssignmentActionNode, *ast.SendStatement, *ast.TerminateStatement: // An action node member written as a statement (`then send x via p;`, @@ -1967,15 +1983,17 @@ func (e *ActionExecutor) enabledSuccessions(frame *actionFrame, node ast.Node) ( // guardHolds evaluates the guard a succession out of node carries; a succession // carrying none is unconditional. func (e *ActionExecutor) guardHolds(ec *EvalContext, node, guard ast.Node) (bool, error) { - result, err := guardResult(ec, guard) - if err != nil { - return false, fmt.Errorf("eval guard of %s: %w", nodeDescription(node), err) - } - if !result.isBool() { - return false, fmt.Errorf("%w: %s: guard must evaluate to boolean, got %v", - ErrTypeMismatch, nodeDescription(node), result.Kind) - } - return result.Const.Bool, nil + return e.ctx.guardUnderStatementOrders(guard, e.stepCount+1, ec.scope, func() (bool, error) { + result, err := guardResult(ec, guard) + if err != nil { + return false, fmt.Errorf("eval guard of %s: %w", nodeDescription(node), err) + } + if !result.isBool() { + return false, fmt.Errorf("%w: %s: guard must evaluate to boolean, got %v", + ErrTypeMismatch, nodeDescription(node), result.Kind) + } + return result.Const.Bool, nil + }) } // guardResult is what a guard evaluates to; no guard is true. @@ -1990,21 +2008,34 @@ func guardResult(ec *EvalContext, guard ast.Node) (Value, error) { // node whose branch is already decided, as a probe the context undoes whole: the // read reports a choice and leaves the run as it was. A guard with no result is // noted and not selected. -func (e *ActionExecutor) probeGuard(frame *actionFrame, node *ast.DecisionNode, successors []lower.ActionEdge, i int) bool { - result, err := func() (Value, error) { +func (e *ActionExecutor) probeGuard(frame *actionFrame, node *ast.DecisionNode, successors []lower.ActionEdge, i int) (bool, error) { + guard := successors[i].Guard + scope := e.graphOf(frame).Scope + var holds bool + var err error + func() { defer e.ctx.beginProbe()() - ec := e.evalContextFor(frame, e.graphOf(frame).Scope) - defer ec.beginStep()() - return guardResult(ec, successors[i].Guard) + holds, err = e.ctx.guardUnderStatementOrders(guard, e.stepCount+1, scope, func() (bool, error) { + ec := e.evalContextFor(frame, e.graphOf(frame).Scope) + defer ec.beginStep()() + result, err := guardResult(ec, guard) + if err != nil { + return false, err + } + if !result.isBool() { + return false, fmt.Errorf("%w: guard must evaluate to boolean, got %v", ErrTypeMismatch, result.Kind) + } + return result.Const.Bool, nil + }) }() - if err == nil && !result.isBool() { - err = fmt.Errorf("%w: guard must evaluate to boolean, got %v", ErrTypeMismatch, result.Kind) - } if err != nil { + if errors.Is(err, ErrOrderDependentPreview) || errors.Is(err, ErrOrderDependentGuardEffect) { + return false, err + } e.noteUnevaluableGuard(frame, node, successors, i, err) - return false + return false, nil } - return result.Const.Bool + return holds, nil } // scheduleTokens hands the step the tokens it may move, those eligible now, in @@ -2392,7 +2423,10 @@ func (e *ActionExecutor) stepDecisionNode(tokenIdx int) error { var holds bool if len(holding) > 0 { - holds = e.probeGuard(token.frame, decisionNode, successors, i) + var err error + if holds, err = e.probeGuard(token.frame, decisionNode, successors, i); err != nil { + return err + } } else { var err error if holds, err = e.guardHolds(ec, decisionNode, edge.Guard); err != nil { @@ -2562,7 +2596,7 @@ func (e *ActionExecutor) stepNestedAction(tokenIdx int) error { // node until the trigger is ready. func (e *ActionExecutor) awaitTrigger(token *Token, accept lower.Accept) (bool, error) { ready, err := e.triggerReady(token, accept) - if ready || err != nil { + if _, change := accept.Trigger.(*ast.ChangeEvent); !change { ready, err = e.triggerHolds(token, accept) } if err != nil { @@ -2712,13 +2746,11 @@ func (e *ActionExecutor) advance(tokenIdx int, successors []lower.ActionEdge) er return nil } -// triggerReady probes a change event's condition first: a test finding it not -// holding is no move and leaves no trace. A time event parks visibly, so it is not probed. +// triggerReady reports whether a change event's condition currently holds. func (e *ActionExecutor) triggerReady(token *Token, accept lower.Accept) (bool, error) { if _, changes := accept.Trigger.(*ast.ChangeEvent); !changes { return true, nil } - defer e.ctx.beginProbe()() return e.triggerHolds(token, accept) } @@ -2733,14 +2765,16 @@ func (e *ActionExecutor) triggerHolds(token *Token, accept lower.Accept) (bool, case *ast.ChangeEvent: ec := e.evalContextFor(frame, frame.graph.Scope) defer ec.beginStep()() - result, err := ec.Eval(t.Condition) - if err != nil { - return false, fmt.Errorf("eval accept condition: %w", err) - } - if result.Kind != ValConst || result.Const.Kind != semantics.ValBool { - return false, fmt.Errorf("%w: accept when: condition must evaluate to boolean, got %v", ErrTypeMismatch, result.Kind) - } - return result.Const.Bool, nil + return e.ctx.guardUnderStatementOrders(t.Condition, e.stepCount+1, frame.graph.Scope, func() (bool, error) { + result, err := ec.Eval(t.Condition) + if err != nil { + return false, fmt.Errorf("eval accept condition: %w", err) + } + if result.Kind != ValConst || result.Const.Kind != semantics.ValBool { + return false, fmt.Errorf("%w: accept when: condition must evaluate to boolean, got %v", ErrTypeMismatch, result.Kind) + } + return result.Const.Bool, nil + }) case *ast.TimeEvent: if token.Wait != nil && token.Wait.Timed { return e.ctx.clock.now >= token.Wait.Due, nil @@ -3020,6 +3054,8 @@ func statementNodeKeyword(node ast.Node) string { return "a 'send'" case *ast.TerminateStatement: return "a 'terminate'" + case *ast.PerformActionNode: + return "a 'perform'" case *ast.Usage: return "the assertion " + ActionNodeName(n) default: diff --git a/internal/exec/runtime/action_frame.go b/internal/exec/runtime/action_frame.go index 42139d110b..b71b7338e2 100644 --- a/internal/exec/runtime/action_frame.go +++ b/internal/exec/runtime/action_frame.go @@ -35,6 +35,27 @@ type performances struct { // flow is the executor holding the tokens these performances run under, which a // terminate drops when it ends one of them. flow *ActionExecutor + // orders caches lower.BodyStatementOrder by the statement list's first element. + orders map[*lower.Statement]*lower.StatementOrder +} + +// statementOrder is lower.BodyStatementOrder of stmts, statements of node in graph. +func (e *performances) statementOrder(graph *lower.ActionGraph, node ast.Node, stmts []lower.Statement) *lower.StatementOrder { + key := &stmts[0] + order, known := e.orders[key] + if !known { + if graph != nil { + order = graph.StatementOrders[node] + } + if order == nil { + order = lower.BodyStatementOrder(graph, node, stmts) + } + if e.orders == nil { + e.orders = make(map[*lower.Statement]*lower.StatementOrder) + } + e.orders[key] = order + } + return order } // performanceOwner is the behavior whose nodes perform — an action executor or a state @@ -1602,6 +1623,9 @@ func (e *performances) performInvocation(perf *actionFrame, inv actionInvocation if callee, err = e.beginInvocation(perf, inv); err != nil { return err } + if err := e.ctx.startShotMove(callee); err != nil { + return err + } } if resumed { callee.exec.listen(perf, e.streamCalleeOutput(perf, callee.out)) diff --git a/internal/exec/runtime/action_holds.go b/internal/exec/runtime/action_holds.go index 0b592829f9..1204bf8e91 100644 --- a/internal/exec/runtime/action_holds.go +++ b/internal/exec/runtime/action_holds.go @@ -18,8 +18,12 @@ func (e *ActionExecutor) Holds(sym *symbols.Symbol, scope *symbols.Scope) (bool, return false, fmt.Errorf("%w: no condition named", ErrNoConditions) } defer e.ctx.beginExecutorRun(&e.driven)() - defer e.ctx.beginProbe()() + return e.ctx.everyStatementOrder(func() (bool, error) { + return e.holds(sym, scope) + }) +} +func (e *ActionExecutor) holds(sym *symbols.Symbol, scope *symbols.Scope) (bool, error) { kind, what := "constraint", "assertion" if err := RequireConstraint(sym); err != nil { if RequireRequirement(sym) != nil { diff --git a/internal/exec/runtime/action_statements.go b/internal/exec/runtime/action_statements.go index 27825640bb..32fdb22773 100644 --- a/internal/exec/runtime/action_statements.go +++ b/internal/exec/runtime/action_statements.go @@ -16,13 +16,15 @@ type actionStmtHost struct { node ast.Node // the action node whose body is running, for diagnostics // perf is the performance the body runs in, whose features it declares into. perf *actionFrame + // graph is the flow node is a node of. + graph *lower.ActionGraph } // executeBody runs the lowered statements graph records for node in perf, the // performance they belong to, with the performances around it in lexical reach. func (e *performances) executeBody(perf *actionFrame, graph *lower.ActionGraph, node ast.Node) error { _, err := e.ctx.runStatements(func() *stmtEngine { - host := &actionStmtHost{exec: e, node: node, perf: perf} + host := &actionStmtHost{exec: e, node: node, perf: perf, graph: graph} lexical := perf.lexicalFrames() return newStmtEngineIn(e.ctx, host, lexical[len(lexical)-1], lexical[:len(lexical)-1]) }, graph.Bodies[node]) @@ -126,6 +128,28 @@ func (h *actionStmtHost) materializeOccurrence() (*Instance, error) { } // acceptReturn rejects a `return`: an action node computes no result to return. +// statementOrder is how stmts may be ordered where the schedule picks it: a body's +// statements are subactions no succession orders. +func (h *actionStmtHost) statementOrder(stmts []lower.Statement) *lower.StatementOrder { + if len(stmts) < 2 { + return nil + } + order := h.exec.statementOrder(h.graph, h.node, stmts) + if h.exec.ctx.scheduling().ordersStatements() || order.HasReversePrecedence() || order.HasSkipped() { + return order + } + return nil +} + +func (h *actionStmtHost) orderStep() int { + if h.exec.flow == nil { + return 0 + } + return h.exec.flow.stepCount + 1 +} + +func (h *actionStmtHost) yieldsBetweenStatements() bool { return true } + func (h *actionStmtHost) acceptReturn(Value, lower.Return) error { return fmt.Errorf("%w: %s", ErrReturnOutsideCalc, h.describe()) } diff --git a/internal/exec/runtime/action_subflow.go b/internal/exec/runtime/action_subflow.go index ff873381b3..c1bd843205 100644 --- a/internal/exec/runtime/action_subflow.go +++ b/internal/exec/runtime/action_subflow.go @@ -203,11 +203,14 @@ func (e *ActionExecutor) driveSubflow(f *subflowFrame) error { e.ctx.bodyPerformed() } if moved { + if err := e.ctx.switchStrand(); err != nil { + return err + } switch { case e.ctx.stepsTokens(): // A run one move at a time pauses before its next, its machine going on meanwhile. if e.canAct(perf) { - if err := e.ctx.tokenStepBody(); err != nil { + if err := e.ctx.tokenStepBody(perf.graph); err != nil { return err } } @@ -253,10 +256,15 @@ func (e *ActionExecutor) stepSubflow(perf *actionFrame) (moved, performed bool, before := e.subflowLocations(perf) performing := e.performingTokens(perf) if !e.ctx.stepsTokens() { + pauses := e.pauses if err := e.stepSubflowSweep(perf); err != nil { return false, false, err } - return e.subflowMoved(perf, before, performing) + moved, performed, err := e.subflowMoved(perf, before, performing) + if e.yieldedIn(perf, pauses) { + moved, performed = true, true + } + return moved, performed, err } // A token moving on a loop may stand where it stood, so the move itself counts. acted, performed, err := e.stepSubflowMove(perf) @@ -273,13 +281,15 @@ func (e *ActionExecutor) stepSubflowSweep(perf *actionFrame) (err error) { defer e.beginSweep()() order := e.beginStepOrder() endWrites := e.beginStepWrites(e.stepCount + 1) - eligible := func(t Token) bool { return t.inFlowOf(perf) } + eligible := func(t Token) bool { return t.inFlowOf(perf) && !t.drivenUnder(perf) } if e.ctx.scheduling().oneMove() { // Paused work that would only pause again is no alternative to pick. - eligible = func(t Token) bool { return t.inFlowOf(perf) && (t.body == nil || t.resumable()) } + eligible = func(t Token) bool { + return t.inFlowOf(perf) && !t.drivenUnder(perf) && (t.body == nil || t.resumable()) + } } candidates := e.stepCandidates(&order, eligible, perf) - schedule := e.ctx.scheduling().scheduleStep(candidates) + schedule := e.scheduleSubflowStep(perf, candidates) for id, ok := schedule.Next(); ok; id, ok = schedule.Next() { i := e.tokenIndex(id) if i < 0 || e.moving(e.tokens[i]) || !e.tokens[i].inFlowOf(perf) { @@ -301,6 +311,18 @@ func (e *ActionExecutor) stepSubflowSweep(perf *actionFrame) (err error) { return err } +// An unordered case body keeps declaration order under the fixed schedules. +func (e *ActionExecutor) scheduleSubflowStep(perf *actionFrame, candidates stepTokens) *tokenSchedule { + scheduler := e.ctx.scheduling() + if perf.graph != nil && perf.graph.UnstatedCaseFlow && scheduler.oneMove() { + candidates.stepped = true + } + if perf.graph != nil && perf.graph.UnstatedCaseFlow && scheduler.policy.kind == scheduleReverse { + return &tokenSchedule{order: candidates.ids} + } + return scheduler.scheduleStep(candidates) +} + // stepSubflowMove is the step of a flow run one token move at a time: its silent // moves settle, without a draw, around one move drawn among the tokens able to act; // it reports whether a token acted and whether the drawn one performed its node. @@ -328,7 +350,7 @@ func (e *ActionExecutor) stepSubflowMove(perf *actionFrame) (acted, performed bo func (e *ActionExecutor) drawOneMove(perf *actionFrame) (acted, performed bool, err error) { defer e.beginSweep()() order := e.beginStepOrder() - schedule := e.ctx.scheduling().scheduleStep(e.stepCandidates(&order, oneMoveEligibleIn(perf), perf)) + schedule := e.scheduleSubflowStep(perf, e.stepCandidates(&order, oneMoveEligibleIn(perf), perf)) for id, ok := schedule.Next(); ok; id, ok = schedule.Next() { i := e.tokenIndex(id) if i < 0 || e.moving(e.tokens[i]) || !e.tokens[i].inFlowOf(perf) { @@ -355,7 +377,9 @@ func (e *ActionExecutor) drawOneMove(perf *actionFrame) (acted, performed bool, // oneMoveEligibleIn is the eligibility of a step moving one token of perf's flow. func oneMoveEligibleIn(perf *actionFrame) func(Token) bool { - return func(t Token) bool { return t.inFlowOf(perf) && (t.body == nil || t.resumable()) } + return func(t Token) bool { + return t.inFlowOf(perf) && !t.drivenUnder(perf) && (t.body == nil || t.resumable()) + } } // canAct reports whether a token of perf's flow would act were it stepped now. @@ -388,7 +412,7 @@ func (e *ActionExecutor) silentPass(perf *actionFrame) (moved bool, err error) { defer e.beginSweep()() for i := 0; i < len(e.tokens); i++ { t := e.tokens[i] - if e.moving(t) || !t.inFlowOf(perf) || !e.silentMove(t) { + if e.moving(t) || !t.inFlowOf(perf) || t.drivenUnder(perf) || !e.silentMove(t) { continue } did, err := e.stepTokenNoting(i, &stepOrder{}) @@ -443,6 +467,17 @@ func (e *ActionExecutor) subflowMoved(perf *actionFrame, before map[int64]ast.No return moved, performed, nil } +// yieldedIn reports a token of perf's flow whose work yielded after the executor's +// pauses-th pause, a move of its node made. +func (e *ActionExecutor) yieldedIn(perf *actionFrame, pauses int64) bool { + for _, idx := range e.tokensIn(perf) { + if run := e.tokens[idx].body; run != nil && run.pausedAt > pauses && run.paused.yielded { + return true + } + } + return false +} + // subflowLocations returns where each token of perf's flow sits, by token ID. func (e *ActionExecutor) subflowLocations(perf *actionFrame) map[int64]ast.Node { locations := make(map[int64]ast.Node) @@ -511,6 +546,16 @@ func (t Token) inFlowOf(perf *actionFrame) bool { return false } +// drivenUnder reports a token of a flow nested in perf's that another token's work drives. +func (t Token) drivenUnder(perf *actionFrame) bool { + for f := t.frame; f != nil && f != perf; f = f.parent { + if f.inBody { + return true + } + } + return false +} + // positionIn returns the node of perf's flow the token stands at: its location, or the node // owning the flow nested under perf it runs in; false for a token outside perf's flow. func (t Token) positionIn(perf *actionFrame) (ast.Node, bool) { diff --git a/internal/exec/runtime/action_terminate.go b/internal/exec/runtime/action_terminate.go index 998bb06c8a..0d142eda61 100644 --- a/internal/exec/runtime/action_terminate.go +++ b/internal/exec/runtime/action_terminate.go @@ -118,7 +118,7 @@ func (e *performances) terminateTargets(perf *actionFrame, s lower.Effect) ([]*a return []*actionFrame{perf.parent}, nil case lower.TerminateNode: for f := perf; f != nil; f = f.parent { - if f.node == s.Target { + if f.node == s.Target && !f.body { ongoing := e.ongoingWith(f, s.Target) pending, err := e.flow.beginPending(f.parent, s.Target) return append(ongoing, pending...), err @@ -247,10 +247,25 @@ func (t Token) performed() []*actionFrame { if w, ok := t.body.work.(*usageWork); ok { held = append(held, w.perf) } - cursor := t.body.cursor + return append(held, performedIn(t.body.cursor)...) +} + +// performedIn returns the performances the frames of cursor perform, outermost first, +// with those of the statements an unordered list among them set aside. +func performedIn(cursor []bodyFrame) []*actionFrame { + var held []*actionFrame for i := len(cursor) - 1; i >= 0; i-- { - if f, ok := cursor[i].(*performFrame); ok && f.perf != nil { - held = append(held, f.perf) + switch f := cursor[i].(type) { + case *performFrame: + if f.perf != nil { + held = append(held, f.perf) + } + case *stmtListFrame: + for _, s := range f.strands { + if s != nil { + held = append(held, performedIn(s.cursor)...) + } + } } } return held diff --git a/internal/exec/runtime/calc_statements.go b/internal/exec/runtime/calc_statements.go index 78c3b17002..d9f99af055 100644 --- a/internal/exec/runtime/calc_statements.go +++ b/internal/exec/runtime/calc_statements.go @@ -202,6 +202,36 @@ func (h *calcStmtHost) assignForeign(_ *EvalContext, s lower.Assign, _ Value) er // acceptReturn takes the value a `return` yields, which the result parameter // then holds, so it answers to that parameter's declaration. +func (h *calcStmtHost) statementOrder(stmts []lower.Statement) *lower.StatementOrder { + if h.shape.performs() || len(stmts) < 2 { + return nil + } + key := &stmts[0] + if order, ok := h.shape.statementOrders.Load(key); ok { + order := order.(*lower.StatementOrder) + if h.ctx.scheduling().ordersStatements() || order.HasReversePrecedence() || order.HasSkipped() { + return order + } + return nil + } + order := lower.CalcBodyStatementOrder(h.shape.bodyScope(), nil, stmts) + actual, _ := h.shape.statementOrders.LoadOrStore(key, order) + order = actual.(*lower.StatementOrder) + if h.ctx.scheduling().ordersStatements() || order.HasReversePrecedence() || order.HasSkipped() { + return order + } + return nil +} + +func (h *calcStmtHost) orderStep() int { + if h.flow != nil { + return h.flow.stepCount + 1 + } + return h.ctx.enclosingExecutorStep() +} + +func (h *calcStmtHost) yieldsBetweenStatements() bool { return false } + func (h *calcStmtHost) acceptReturn(value Value, _ lower.Return) error { if out := h.shape.resultOutput(); out != nil { if err := out.Decl.check(h.ctx, &value, func() string { return "result" }); err != nil { @@ -273,9 +303,16 @@ func (h *calcStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, return flowNext, fmt.Errorf("%s: a binding or flow at a pin of %s in a body is not executable", h.describe(), nodeDescription(node)) } + var nestedSteps []lower.Statement + var nestedOrder *lower.StatementOrder if sub, owns := graph.Subflows[node]; owns && sub != nil { - return flowNext, fmt.Errorf("%s: the flow %s states of its own in a body is not executable", - h.describe(), nodeDescription(node)) + var simple bool + nestedSteps, simple = sub.Graph.StatementList() + if !simple { + return flowNext, fmt.Errorf("%s: the flow %s states of its own in a body is not executable", + h.describe(), nodeDescription(node)) + } + nestedOrder = graph.StatementOrders[node] } engine.env.enter() defer engine.env.leave() @@ -291,6 +328,9 @@ func (h *calcStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, } engine.env.declare(feature.Name, value) } + if nestedSteps != nil { + return engine.runWithOrder(nestedSteps, nestedOrder) + } return engine.run(graph.Bodies[node]) } @@ -298,6 +338,11 @@ func (h *calcStmtHost) runBlockFlow(engine *stmtEngine, block lower.Block) (stmt if h.perfs != nil { return h.perfs.performBlockFlow(h.perfs.root, engine, block) } + if block.Stated { + if steps, ok := block.Graph.StatementList(); ok { + return engine.runWithOrder(steps, block.Order) + } + } return flowNext, fmt.Errorf("%w: %s: the flow a body states in a calculation is not executable", ErrStatementNotExecutable, h.describe()) } diff --git a/internal/exec/runtime/calc_usage.go b/internal/exec/runtime/calc_usage.go index b8e3c5a34d..4c17cf61a7 100644 --- a/internal/exec/runtime/calc_usage.go +++ b/internal/exec/runtime/calc_usage.go @@ -130,21 +130,6 @@ func indexOfAnonymousResult(outs []calcOutput) (int, bool) { return 0, false } -// calcSteps is the computation an invoked calc runs: its lowered body without -// the value bindings of its `out` features. An `out` binding states what that -// feature is, not a step of the body, so a calc declaring several of them -// returns none of them by falling off the end of its body. -func calcSteps(body []lower.Statement) []lower.Statement { - steps := make([]lower.Statement, 0, len(body)) - for _, stmt := range body { - if ret, ok := stmt.(lower.Return); ok && isOutputBinding(ret.Node) { - continue - } - steps = append(steps, stmt) - } - return steps -} - // assignedOutputs are the outputs the body's statements assign, on any path // through it: what the calc computes, whichever way an execution branches. func assignedOutputs(stmts []lower.Statement, outputs []calcOutput, aliases map[string]string) map[string]bool { @@ -187,14 +172,6 @@ func collectAssignedOutputs(stmts []lower.Statement, declared map[string]string, } } -// isOutputBinding reports whether a lowered return states an `out` feature's -// value rather than a `return`. A result parameter stays a return: it is the -// one value the calc designates. -func isOutputBinding(node ast.Node) bool { - usage, ok := node.(*ast.Usage) - return ok && usage.Direction == ast.DirOut && !usage.IsResult -} - // output finds the output feature of that name. func (shape *calcShape) output(name string) (calcOutput, bool) { if name == "" { diff --git a/internal/exec/runtime/case_step_order_test.go b/internal/exec/runtime/case_step_order_test.go new file mode 100644 index 0000000000..d731d8b1dd --- /dev/null +++ b/internal/exec/runtime/case_step_order_test.go @@ -0,0 +1,214 @@ +package runtime + +import ( + "context" + "os" + "path/filepath" + "slices" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +func TestAnalysisCaseStepOrderOutcomes(t *testing.T) { + m := caseStepOrderModel(t, "analysis_explore_step_order") + sym := namedOrFoundSymbol(t, m.idx, "test::An", m.idx.DocumentRoot(m.path), ast.DefAnalysisCase, ast.UsageAnalysisCase) + exploration := exploreCaseAnalysis(t, m, sym) + if !exploration.Complete() || exploration.Runs != 2 { + t.Fatalf("exploration %s after %d runs, want two complete runs", exploration.Status(), exploration.Runs) + } + if got := caseResultValues(t, exploration, "result"); !slices.Equal(got, []string{"12", "30"}) { + t.Fatalf("analysis outcomes are %v, want [12 30]", got) + } + for _, outcome := range exploration.Outcomes { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, ReplayPolicy(outcome.Witness)) + result, err := ctx.RunAnalysis(sym, AnalysisArgs{}, m.idx.DocumentRoot(m.path), nil) + if err == nil { + err = ctx.Unfollowed() + } + if err != nil { + t.Fatalf("replay of %s: %v", FormatChoices(outcome.Witness), err) + } + if got := analysisResultValue(t, ctx, result, "result"); got != FormatValue(outcome.Outcome.Outputs["result"]) { + t.Errorf("replay of %s produced %s, want %s", + FormatChoices(outcome.Witness), got, FormatValue(outcome.Outcome.Outputs["result"])) + } + } + + for _, schedule := range []struct { + policy string + want string + }{ + {policy: "reverse", want: "12"}, + {policy: "declared", want: "12"}, + } { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, schedule.policy)) + result, err := ctx.RunAnalysis(sym, AnalysisArgs{}, m.idx.DocumentRoot(m.path), nil) + if err != nil { + t.Fatalf("%s analysis run: %v", schedule.policy, err) + } + if got := analysisResultValue(t, ctx, result, "result"); got != schedule.want { + t.Errorf("%s result = %s, want %s", schedule.policy, got, schedule.want) + } + } +} + +func TestAnalysisCaseStepOrderCheckOracle(t *testing.T) { + m := caseStepOrderModel(t, "analysis_explore_step_order") + want := loadCheckExpected(t, "analysis_explore_step_order") + report := checkStart(t, m, starterOf(m.action(t, "Use")), + CheckOptions{Reduce: true, Diverge: []string{"r"}}) + if got := report.Verdict.String(); got != want.Verdict { + t.Fatalf("check verdict = %s (%+v), want %s", got, report, want.Verdict) + } + for feature, values := range want.Divergent { + if got := divergentValues(report, feature); !slices.Equal(got, values) { + t.Errorf("%s diverges over %v, want %v", feature, got, values) + } + } +} + +func TestAnalysisCaseStepOrderCommutingAndStatedFlow(t *testing.T) { + for _, tc := range []struct { + name string + fqn string + want []string + runs int + }{ + {name: "analysis_case_step_order_commuting", fqn: "test::Commute", want: []string{"3"}, runs: 2}, + {name: "analysis_case_step_order_stated", fqn: "test::Ordered", want: []string{"12"}, runs: 1}, + } { + t.Run(tc.name, func(t *testing.T) { + m := caseStepOrderModel(t, tc.name) + sym := namedOrFoundSymbol(t, m.idx, tc.fqn, m.idx.DocumentRoot(m.path), ast.DefAnalysisCase, ast.UsageAnalysisCase) + exploration := exploreCaseAnalysis(t, m, sym) + if !exploration.Complete() || exploration.Runs != tc.runs { + t.Fatalf("exploration %s after %d runs, want %d complete run(s)", + exploration.Status(), exploration.Runs, tc.runs) + } + if got := caseResultValues(t, exploration, "result"); !slices.Equal(got, tc.want) { + t.Fatalf("analysis outcomes are %v, want %v", got, tc.want) + } + t.Logf("exploration reached %d outcome(s) in %d run(s)", len(exploration.Outcomes), exploration.Runs) + }) + } +} + +func TestVerificationCaseStepOrderOutcomes(t *testing.T) { + m := caseStepOrderModel(t, "verification_explore_step_order") + sym := namedOrFoundSymbol(t, m.idx, "test::OrderCheck", m.idx.DocumentRoot(m.path), ast.DefVerificationCase, ast.UsageVerificationCase) + policy, err := ExplorePolicy(DefaultExploreBudget) + if err != nil { + t.Fatal(err) + } + exploration, err := Explore(context.Background(), policy, m.fresh, func(ctx *Context) (Outcome, error) { + result, err := ctx.RunVerification(sym, AnalysisArgs{}, m.idx.DocumentRoot(m.path), nil) + if err != nil { + return Outcome{}, err + } + return Outcome{Outputs: map[string]Value{ + "verdict": NewStringValue(string(result.Verdict.Kind)), + }}, nil + }) + if err != nil { + t.Fatalf("explore verification case: %v", err) + } + if !exploration.Complete() { + t.Fatalf("verification exploration %s", exploration.Status()) + } + got := make([]string, len(exploration.Outcomes)) + for i, outcome := range exploration.Outcomes { + value, ok := outcome.Outcome.Outputs["verdict"] + if !ok { + t.Fatalf("verification outcome %s has no verdict", outcome.Outcome) + } + got[i] = FormatValue(value) + } + slices.Sort(got) + if !slices.Equal(got, []string{`"fail"`, `"pass"`}) { + t.Fatalf("verification verdicts are %v, want [fail pass]", got) + } +} + +func TestVerificationCaseStepOrderCheckFindsViolation(t *testing.T) { + m := caseStepOrderModel(t, "verification_explore_step_order") + property := CheckProperty{ + Name: "verification case passes", + Holds: func(_ *Context, inv *Invocation) (bool, error) { + value, ok := inv.Actions[0].Results()["verdict"] + if !ok { + return true, nil + } + return FormatValue(value) == "VerdictKind::pass", nil + }, + } + report := checkStart(t, m, starterOf(m.action(t, "Harness")), reduced(), property) + for _, violation := range report.Violations { + if violation.Kind == ViolationProperty && violation.Name == property.Name { + return + } + } + t.Fatalf("check %+v found no violation of the order-dependent verification verdict", report) +} + +func exploreCaseAnalysis(t *testing.T, m *exploreModel, sym *symbols.Symbol) *Exploration { + t.Helper() + policy, err := ExplorePolicy(DefaultExploreBudget) + if err != nil { + t.Fatal(err) + } + exploration, err := Explore(context.Background(), policy, m.fresh, func(ctx *Context) (Outcome, error) { + result, err := ctx.RunAnalysis(sym, AnalysisArgs{}, m.idx.DocumentRoot(m.path), nil) + if err != nil { + return Outcome{}, err + } + outputs := make(map[string]Value, len(result.Outputs)) + for _, output := range result.Outputs { + outputs[output.Name] = output.Value + } + return Outcome{Outputs: outputs}, nil + }) + if err != nil { + t.Fatalf("explore analysis case: %v", err) + } + return exploration +} + +func caseStepOrderModel(t *testing.T, name string) *exploreModel { + t.Helper() + data, err := os.ReadFile(filepath.Join("testdata", "conformance", name+".sysml")) + if err != nil { + t.Fatal(err) + } + return parseLibraryModel(t, string(data)) +} + +func caseResultValues(t *testing.T, exploration *Exploration, name string) []string { + t.Helper() + values := make([]string, len(exploration.Outcomes)) + for i, outcome := range exploration.Outcomes { + values[i] = outcomeValue(t, outcome.Outcome, name) + } + slices.Sort(values) + return values +} + +func analysisResultValue(t *testing.T, ctx *Context, result AnalysisResult, name string) string { + t.Helper() + for _, output := range result.Outputs { + if output.Name == name { + return FormatValue(output.Value) + } + } + t.Fatalf("analysis result has no output %q", name) + return "" +} diff --git a/internal/exec/runtime/check.go b/internal/exec/runtime/check.go index 80eb126d30..c703971c59 100644 --- a/internal/exec/runtime/check.go +++ b/internal/exec/runtime/check.go @@ -190,7 +190,7 @@ type CheckReport struct { States int Moves int MaxDepth int - // BoundsHit names the bounds the search ran into: `depth`, `states`, and the + // BoundsHit names the search and statement-order bounds it ran into, and the // executor's budgets by name (ExecutorBounds); none when exhaustive. BoundsHit []string // Limits are the executor's budgets the search ran under. @@ -865,8 +865,14 @@ func ownerUnits(moves []searchMove) map[checkedExecutor]int { // evaluate asks the property of the state under a probe: what evaluating it // derives is given back. func (c *checker) evaluate(p CheckProperty) (bool, error) { - defer c.ctx.beginProbe()() - return p.Holds(c.ctx, c.inv) + holds, err := c.ctx.everyStatementOrder(func() (bool, error) { + return p.Holds(c.ctx, c.inv) + }) + if errors.Is(err, ErrStatementOrderSweepLimit) { + c.hit(BoundStatementOrders) + return holds, nil + } + return holds, err } // scopeWith is scope with the reasons of more it lacks, sorted. @@ -883,24 +889,61 @@ func scopeWith(scope, more []ObservationReason) []ObservationReason { // final records the outcome of a complete schedule, the first schedule // reaching each distinct outcome being its witness. func (c *checker) final() { - values, spelled, identity, scope := c.spellFinal() - c.scope = scopeWith(c.scope, scope) - if _, seen := c.finals[identity]; seen { - return + finals, err := c.spellFinalVariants() + if errors.Is(err, ErrStatementOrderSweepLimit) { + c.hit(BoundStatementOrders) + } + for _, final := range finals { + c.scope = scopeWith(c.scope, final.scope) + if _, seen := c.finals[final.identity]; seen { + continue + } + c.finals[final.identity] = len(c.results) + witness := c.witness() + for _, choice := range final.choices { + choice.Where += finalOrderChoiceSuffix + witness.Choices = append(witness.Choices, choice) + } + c.results = append(c.results, CheckFinal{ + Outcome: final.spelled, + Values: final.values, + Witness: witness, + identity: final.identity, + }) } - c.finals[identity] = len(c.results) - c.results = append(c.results, CheckFinal{ - Outcome: spelled, - Values: values, - Witness: c.witness(), - identity: identity, - }) } // spellFinal renders the completed state's outcome and divergence values under a probe; // a selected performer feature the outcome leaves out (an item, one unset or in error) joins both. func (c *checker) spellFinal() (values map[string]string, spelled, identity string, scope []ObservationReason) { - defer c.ctx.beginProbe()() + finals, _ := c.spellFinalVariants() + if len(finals) == 0 { + return nil, "", "", nil + } + return finals[0].values, finals[0].spelled, finals[0].identity, finals[0].scope +} + +type spelledCheckFinal struct { + values map[string]string + spelled string + identity string + scope []ObservationReason + choices []ChoiceTaken +} + +func (c *checker) spellFinalVariants() ([]spelledCheckFinal, error) { + var finals []spelledCheckFinal + err := c.ctx.sweepStatementOrderVariants(func(sweep *statementOrderSweep) error { + values, spelled, identity, scope := c.spellFinalOnce() + finals = append(finals, spelledCheckFinal{ + values: values, spelled: spelled, identity: identity, scope: scope, choices: slices.Clone(sweep.choices), + }) + return nil + }) + return finals, err +} + +func (c *checker) spellFinalOnce() (values map[string]string, spelled, identity string, scope []ObservationReason) { outcome := c.inv.Outcome() values = c.divergenceValues() spelled, identity, scope = outcome.String(), outcome.identity(), outcome.Scope @@ -1384,12 +1427,13 @@ func divergences(finals []CheckFinal) []Divergence { // The executor budgets a search runs under, by the name a bound hit reports; // each names one field of Budgets, so a report spells the limit that stopped it. const ( - BoundSteps = "steps" - BoundActionSteps = "actionSteps" - BoundEvents = "events" - BoundDoSteps = "doSteps" - BoundElements = "elements" - BoundBehaviors = "behaviors" + BoundSteps = "steps" + BoundActionSteps = "actionSteps" + BoundEvents = "events" + BoundDoSteps = "doSteps" + BoundElements = "elements" + BoundBehaviors = "behaviors" + BoundStatementOrders = "statement orders" ) // ExecutorBounds lists the executor budgets a bound hit may name, in report order. diff --git a/internal/exec/runtime/check_body.go b/internal/exec/runtime/check_body.go index 6f36350d2c..7a9ee83295 100644 --- a/internal/exec/runtime/check_body.go +++ b/internal/exec/runtime/check_body.go @@ -92,7 +92,38 @@ func (f *engineFrame) spell(s *stateSpeller) string { return "engine{" + s.localFrames(f.engine.env.frames, f.engine.env.unvalued) + "}" } -func (f *stmtListFrame) spell(*stateSpeller) string { return fmt.Sprintf("stmt %d", f.i) } +func (f *stmtListFrame) spell(s *stateSpeller) string { + if f.done == nil { + return fmt.Sprintf("stmt %d", f.i) + } + var b strings.Builder + fmt.Fprintf(&b, "stmt %d of ", f.i) + for i, done := range f.done { + switch { + case done: + b.WriteByte('+') + case f.blocked[i]: + b.WriteByte('!') + default: + b.WriteByte('-') + } + } + if f.switched >= 0 { + fmt.Fprintf(&b, " after %d", f.switched) + } + for i, strand := range f.strands { + if strand == nil { + continue + } + fmt.Fprintf(&b, " [%d:", i) + for _, inner := range strand.cursor { + b.WriteString(" ") + b.WriteString(inner.spell(s)) + } + b.WriteString("]") + } + return b.String() +} func (f *branchFrame) spell(*stateSpeller) string { if f.elseBranch { diff --git a/internal/exec/runtime/check_corpus_test.go b/internal/exec/runtime/check_corpus_test.go index 1822865180..b46aa6dc77 100644 --- a/internal/exec/runtime/check_corpus_test.go +++ b/internal/exec/runtime/check_corpus_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "os" "path/filepath" + "slices" "sort" "strings" "testing" @@ -29,6 +30,9 @@ type CheckExpected struct { Divergent map[string][]string `json:"divergent,omitempty"` // Agreed lists features every schedule leaves with one value, and that value. Agreed map[string]string `json:"agreed,omitempty"` + // Failures lists every distinct typed runtime error a schedule fails with, each + // matched as a substring, in canonical order; a failure unlisted fails the case. + Failures []string `json:"failures,omitempty"` } // checkCase is one action or state conformance case with an admissible set, ready to check and explore. @@ -203,6 +207,9 @@ func TestCheckConformanceOracles(t *testing.T) { t.Fatalf("reduce=%v: %s diverges over %v, want %v", opts.Reduce, feature, got, values) } } + if got := failureTexts(report); !matchesFailures(got, want.Failures) { + t.Fatalf("reduce=%v: failures %q, want %q", opts.Reduce, got, want.Failures) + } for feature, value := range want.Agreed { for _, final := range report.Finals { got, held := final.Values[feature] @@ -221,12 +228,16 @@ func TestCheckConformanceOracles(t *testing.T) { func TestCheckAgreesWithExploreOverTheConformanceCorpus(t *testing.T) { for _, c := range checkCorpus(t) { t.Run(c.name, func(t *testing.T) { - want := explored(t, c.explore(t)) + x := c.explore(t) + want, failed := exploredFinals(x), exploredFailures(x) for _, opts := range []CheckOptions{reduced(), unreduced()} { report := c.checked(t, opts) - if len(report.Violations) != 0 || len(report.BoundsHit) != 0 { + if len(report.BoundsHit) != 0 || slices.ContainsFunc(report.Violations, func(v Violation) bool { return v.Kind != ViolationFailure }) { t.Fatalf("reduce=%v: %s, violations %v, want a clean complete search", opts.Reduce, report.Status(), report.Violations) } + if got := failureTexts(report); !sameFailures(got, failed) { + t.Fatalf("reduce=%v: check failures\n%s\nexplore failures\n%s", opts.Reduce, strings.Join(got, "\n"), strings.Join(failed, "\n")) + } got := finalOutcomes(report) if strings.Join(got, "\n") != strings.Join(want, "\n") { t.Fatalf("reduce=%v: check finals\n%s\nexplore outcomes\n%s", opts.Reduce, strings.Join(got, "\n"), strings.Join(want, "\n")) @@ -236,6 +247,53 @@ func TestCheckAgreesWithExploreOverTheConformanceCorpus(t *testing.T) { } } +// exploredFinals is the outcome set an exploration reached, its failed runs aside, sorted. +func exploredFinals(x *Exploration) []string { + var out []string + for _, o := range x.Outcomes { + if o.Outcome.Err == nil { + out = append(out, o.Outcome.String()) + } + } + sort.Strings(out) + return out +} + +// exploredFailures is every distinct error an exploration's runs failed with, sorted. +func exploredFailures(x *Exploration) []string { + var out []string + for _, o := range x.Outcomes { + if o.Outcome.Err != nil { + out = append(out, o.Outcome.Err.Error()) + } + } + sort.Strings(out) + return slices.Compact(out) +} + +// failureTexts is every distinct typed runtime error a check's schedules fail with, sorted. +func failureTexts(report *CheckReport) []string { + var out []string + for _, v := range report.Violations { + if v.Kind == ViolationFailure && v.Err != nil { + out = append(out, v.Err.Error()) + } + } + sort.Strings(out) + return slices.Compact(out) +} + +// matchesFailures reports whether each failure contains the expectation at its place. +func matchesFailures(got, want []string) bool { + return slices.EqualFunc(got, want, strings.Contains) +} + +// sameFailures reports whether a check's failures are an exploration's, each run's +// error being the failure the check reports wrapped by the invocation. +func sameFailures(check, explore []string) bool { + return slices.EqualFunc(explore, check, strings.HasSuffix) +} + // explored is the outcome set an exploration reached, sorted as the check sorts its finals. func explored(t *testing.T, x *Exploration) []string { t.Helper() diff --git a/internal/exec/runtime/check_moves.go b/internal/exec/runtime/check_moves.go index 051ac29ab5..1d4d32945f 100644 --- a/internal/exec/runtime/check_moves.go +++ b/internal/exec/runtime/check_moves.go @@ -268,6 +268,9 @@ func (e *StateExecutor) dispatchMoves(d dueDispatch, stepOrder bool) []enabledMo if !d.due { return nil } + if d.fails != nil { + return []enabledMove{{Owner: e, Kind: moveDispatch, Label: d.label, Fails: d.fails}} + } events, label := d.tied, d.label if stepOrder { events, label = d.among, d.step diff --git a/internal/exec/runtime/check_replay.go b/internal/exec/runtime/check_replay.go index 6f78280de7..0c9d0f750b 100644 --- a/internal/exec/runtime/check_replay.go +++ b/internal/exec/runtime/check_replay.go @@ -15,6 +15,8 @@ import ( // ErrReplayDisagrees is the typed error every replay that reaches another state wraps. var ErrReplayDisagrees = errors.New("replay disagrees with the witness") +const finalOrderChoiceSuffix = " (final outcome)" + // ReplayDisagreement reports a replay that left another trace than its witness: // how, and the two traces. type ReplayDisagreement struct { @@ -35,7 +37,30 @@ type Replayed struct { Ctx *Context Inv *Invocation // Err is the error the last move raised, nil when the state is one the run went on from. - Err error + Err error + finalOrders []ChoiceTaken +} + +func splitFinalOrderChoices(w Witness) (Witness, []ChoiceTaken) { + run := cloneWitness(w) + run.Choices = run.Choices[:0] + var final []ChoiceTaken + for _, choice := range w.Choices { + if choice.Kind == ChoiceStatementOrder && strings.HasSuffix(choice.Where, finalOrderChoiceSuffix) { + choice.Where = strings.TrimSuffix(choice.Where, finalOrderChoiceSuffix) + final = append(final, choice) + } else { + run.Choices = append(run.Choices, choice) + } + } + return run, final +} + +func (r *Replayed) withFinalStatementOrders(eval func() error) error { + if len(r.finalOrders) == 0 { + return eval() + } + return r.Ctx.sweepStatementOrdersAt(r.finalOrders, eval) } // Replay re-runs the witness: it starts the invocation start begins in the @@ -66,13 +91,14 @@ func Replay( if err != nil { return nil, err } - if err := ctx.SetSchedule(ReplayOf(w)); err != nil { + runWitness, finalOrders := splitFinalOrderChoices(w) + if err := ctx.SetSchedule(ReplayOf(runWitness)); err != nil { return nil, err } if ctx.Trace() == nil { ctx.SetTrace(NewTraceRecorder()) } - r := &Replayed{Ctx: ctx} + r := &Replayed{Ctx: ctx, finalOrders: finalOrders} run, err := beginInvocation(ctx, start) if err != nil { r.Err = err @@ -170,8 +196,13 @@ func (r *Replayed) agreeOnProperty(w Witness, p CheckProperty) error { // evaluate asks the property of the replayed run under a readiness probe, as the check did. func (r *Replayed) evaluate(p CheckProperty) (bool, error) { - defer r.Ctx.beginProbe()() - return p.Holds(r.Ctx, r.Inv) + holds, err := r.Ctx.everyStatementOrder(func() (bool, error) { + return p.Holds(r.Ctx, r.Inv) + }) + if errors.Is(err, ErrStatementOrderSweepLimit) && !holds { + return false, nil + } + return holds, err } func (r *Replayed) disagree(w Witness, reason string) error { diff --git a/internal/exec/runtime/check_schedule.go b/internal/exec/runtime/check_schedule.go index 71a8791184..b4f4d70a36 100644 --- a/internal/exec/runtime/check_schedule.go +++ b/internal/exec/runtime/check_schedule.go @@ -113,7 +113,9 @@ type checkMove struct { run *checkRun step int selected bool - nested bool // a step of a run within the move, resolved in declared order + nested bool // a step of a run within the move, resolved in declared order + outer *checkMove // the step whose token's move runs this one, restored once it ends + within bool // a step of a flow the outer token's body performs, a choice of its move order []int64 next int moved bool @@ -125,12 +127,17 @@ type checkMove struct { // beginStep resolves the step as a replayed one resolves a witness move: the // selected token alone when two or more are able to act, else — one at most // able to act — that one first and the rest after, as a settling step tries them. -// A step of a do flow within the machine's move picks its token as a choice point -// of the move (ChoiceTokenOrder); any other nested step goes in declared order. +// A step of a do flow within the machine's move, or of a flow a token's body performs +// within its move, picks its token as a choice point of the move (ChoiceTokenOrder); +// any other nested step goes in declared order. func (r *checkRun) beginStep(tokens stepTokens) *checkMove { sameStep := tokens.owner == r.script.owner && tokens.scope == nil m := &checkMove{run: r, step: tokens.step, taken: -1, selected: r.script.token != 0 && sameStep} - if !sameStep { + if outer := r.move; outer != nil && outer.trying() { + m.outer = outer + m.within = tokens.owner == r.script.owner + } + if !sameStep && !m.within { m.nested, m.selected = !tokens.stepped, false } r.move = m @@ -156,7 +163,7 @@ func (r *checkRun) beginStep(tokens stepTokens) *checkMove { m.taken = i } } - if tokens.stepped && !sameStep { + if m.within || tokens.stepped && !sameStep { if len(enabled) >= 2 { m.taken = r.choose(ChoicePoint{Kind: ChoiceTokenOrder, Step: tokens.step, Alternatives: m.enabled}, nil) m.selected = true @@ -195,11 +202,14 @@ func (r *checkRun) beginStep(tokens stepTokens) *checkMove { return m } +// trying reports whether a token the step tried is still making its move. +func (m *checkMove) trying() bool { return m.next > 0 && !m.moved } + // nextToken is the token to try next; false once one acted or none is left, which ends the step. func (m *checkMove) nextToken() (int64, bool) { if m.moved || m.next >= len(m.order) { if m.run.move == m { - m.run.move = nil + m.run.move = m.outer } return 0, false } diff --git a/internal/exec/runtime/choice.go b/internal/exec/runtime/choice.go index e4b2a4a9a9..08c48cce2b 100644 --- a/internal/exec/runtime/choice.go +++ b/internal/exec/runtime/choice.go @@ -47,6 +47,11 @@ const ( // ChoiceEntryStep: an event due for dispatch and a held entry could proceed, // and one of them went first. ChoiceEntryStep + // ChoiceStatementOrder: two or more statements of one body that no succession + // orders could run next, and one of them ran first. + ChoiceStatementOrder + // ChoiceGuardOrder: a guard's result differed among statement orders of a body. + ChoiceGuardOrder ) // String is the kind as a trace or diagnostic names it. @@ -74,6 +79,10 @@ func (k ChoiceKind) String() string { return "step order" case ChoiceEntryStep: return "entry step" + case ChoiceStatementOrder: + return "statement order" + case ChoiceGuardOrder: + return "guard result" } return fmt.Sprintf("ChoiceKind(%d)", int(k)) } @@ -163,8 +172,10 @@ func (c ChoicePoint) Describe() string { return fmt.Sprintf("at %s: due %s (unordered; ran %s first)", c.Where, alts, taken) case ChoiceDispatchOrder: return fmt.Sprintf("%s: %s (unordered; dispatched %s first)", c.Where, alts, taken) - case ChoiceEntryOrder, ChoiceExitOrder, ChoiceStepOrder, ChoiceEntryStep: + case ChoiceEntryOrder, ChoiceExitOrder, ChoiceStepOrder, ChoiceEntryStep, ChoiceStatementOrder: return fmt.Sprintf("%s: next %s (unordered; took %s first)", c.Where, alts, taken) + case ChoiceGuardOrder: + return fmt.Sprintf("%s: verdicts %s (unordered; took %s)", c.Where, alts, taken) } return fmt.Sprintf("%s: %s (unordered; took %s)", c.Kind, alts, taken) } diff --git a/internal/exec/runtime/compile.go b/internal/exec/runtime/compile.go index 812bfcf9ae..b4d6358300 100644 --- a/internal/exec/runtime/compile.go +++ b/internal/exec/runtime/compile.go @@ -203,7 +203,7 @@ func (b *compileBatch) call(member, callee *calcShape) { } // settle withdraws eligibility from every member calling an ineligible shape, -// to a fixpoint, and marks a member reading a library constant via a callee. +// to a fixpoint, and propagates execution requirements through the call graph. func (b *compileBatch) settle() { for changed := true; changed; { changed = false diff --git a/internal/exec/runtime/condition.go b/internal/exec/runtime/condition.go index a1caec5063..aaaa8d8b41 100644 --- a/internal/exec/runtime/condition.go +++ b/internal/exec/runtime/condition.go @@ -7,6 +7,7 @@ import ( "strconv" "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" @@ -32,9 +33,10 @@ type Condition struct { // condition stating an expression. Group []Condition - // Statement is an action statement the body states before its conditions, - // which the evaluator does not execute; nil for a condition or a group. - Statement ast.Node + // Steps are the statements the body performs before its Group's conditions + // are evaluated, as one Boolean function performance; nil for a condition + // or a group stating none. + Steps *BodySteps // Conflict is a second result expression, stated or inherited (KerML // 8.3.4.8); no verdict is reached. Nil otherwise. @@ -54,6 +56,36 @@ type Condition struct { Constraints []*symbols.Symbol } +// BodySteps is the statements one constraint body performs before its +// conditions are evaluated, with the body scope for spans and name resolution. +type BodySteps struct { + Stmts []lower.Statement + Scope *symbols.Scope + Node ast.Node // first statement, for spans + Order *lower.StatementOrder + Footprint lower.Footprint +} + +type constraintBodyKey struct { + node ast.Node + scope *symbols.Scope +} + +func (ctx *Context) constraintBodySteps(stmts []lower.Statement, members []ast.Node, scope *symbols.Scope, node ast.Node) *BodySteps { + body := &BodySteps{Scope: scope, Node: node} + body.Stmts, body.Order = lower.ConstraintBodyWithOrder(scope, members, stmts) + body.Footprint = lower.ConstraintBodyFootprint(scope, body.Stmts) + if node == nil { + return body + } + key := constraintBodyKey{node: node, scope: scope} + if cached, ok := ctx.model.constraintBodies.Load(key); ok { + return cached.(*BodySteps) + } + cached, _ := ctx.model.constraintBodies.LoadOrStore(key, body) + return cached.(*BodySteps) +} + // Label renders the condition as written, negation and grouping included. func (c Condition) Label() string { return conditionLabel(c) } @@ -118,6 +150,18 @@ func (ctx *Context) appendMemberConditions(out []Condition, sym *symbols.Symbol, required bool, seen map[*symbols.Symbol]bool) []Condition { out = ctx.appendResultConflict(out, sym, required) var effective map[*symbols.Symbol]bool + start := len(out) + hasStatements := false + for _, member := range members { + if _, ok := statementKeyword(unwrapBodyMember(member.node)); ok { + hasStatements = true + break + } + } + var steps []lower.Statement + var stepMembers []ast.Node + var stepScope *symbols.Scope + var stepNode ast.Node for _, member := range members { if owner := ctx.namedConstraintOf(member); owner != nil && owner != sym && owner.Name != "" { if effective == nil { @@ -127,8 +171,23 @@ func (ctx *Context) appendMemberConditions(out []Condition, sym *symbols.Symbol, continue } } + if hasStatements { + if stmt, ok := ctx.constraintBodyStep(member.node, member.scope); ok { + if stepNode == nil { + stepNode, stepScope = member.node, member.scope + } + steps = append(steps, stmt) + stepMembers = append(stepMembers, member.node) + continue + } + } out = ctx.appendConditions(out, member.node, member.scope, required, false, seen) } + if len(steps) > 0 { + group := append([]Condition(nil), out[start:]...) + out = append(out[:start], Condition{Group: group, Required: required, + Steps: ctx.constraintBodySteps(steps, stepMembers, stepScope, stepNode)}) + } return out } @@ -187,10 +246,36 @@ func (ctx *Context) appendConditions(out []Condition, node ast.Node, scope *symb return out } var body []Condition + var steps []lower.Statement + var stepMembers []ast.Node + var stepNode ast.Node + hasStatements := bodyHasStatements(m.Body) bodyScope := symbols.ConstraintBodyScope(scope, m) for _, nested := range m.Body { + if hasStatements { + if stmt, ok := ctx.constraintBodyStep(nested, bodyScope); ok { + if stepNode == nil { + stepNode = nested + } + steps = append(steps, stmt) + stepMembers = append(stepMembers, nested) + continue + } + } body = ctx.appendConditions(body, nested, bodyScope, true, false, seen) } + if len(steps) > 0 { + withSteps := Condition{Group: body, Required: required, + Steps: ctx.constraintBodySteps(steps, stepMembers, bodyScope, stepNode)} + if !negated { + for i := range withSteps.Group { + withSteps.Group[i].Required = withSteps.Group[i].Required && required + } + return append(out, withSteps) + } + withSteps.Negated = true + return append(out, withSteps) + } if !negated { for _, c := range body { c.Required = c.Required && required @@ -215,9 +300,9 @@ func (ctx *Context) appendConditions(out []Condition, node ast.Node, scope *symb case *ast.Membership: out = ctx.appendConditions(out, m.Member, scope, required, negated, seen) default: - if _, ok := statementKeyword(m); ok { - out = append(out, Condition{Statement: m, Scope: scope, Required: required}) - } + // A member stating neither a condition nor a step — an expression, a + // declaration — states nothing here; the body's steps were collected by + // the caller that knew the body. } return out } @@ -246,9 +331,29 @@ func (ctx *Context) appendOwnedConditions(out []Condition, member ast.Node, body return out } bodyScope := symbols.ConstraintBodyScope(scope, member) + start := len(out) + hasStatements := bodyHasStatements(body) + var steps []lower.Statement + var stepMembers []ast.Node + var stepNode ast.Node for _, nested := range body { + if hasStatements { + if stmt, ok := ctx.constraintBodyStep(nested, bodyScope); ok { + if stepNode == nil { + stepNode = nested + } + steps = append(steps, stmt) + stepMembers = append(stepMembers, nested) + continue + } + } out = ctx.appendConditions(out, nested, bodyScope, required, false, seen) } + if len(steps) > 0 { + group := append([]Condition(nil), out[start:]...) + out = append(out[:start], Condition{Group: group, Required: required, + Steps: ctx.constraintBodySteps(steps, stepMembers, bodyScope, stepNode)}) + } return out } @@ -288,18 +393,43 @@ func conflictText(conflict *semantics.ResultExpressionConflict) string { } } -// unexecutedStatement returns the first statement conds state, groups included, -// or nil when they state none. -func unexecutedStatement(conds []Condition) ast.Node { - for _, cond := range conds { - if cond.Statement != nil { - return cond.Statement - } - if stmt := unexecutedStatement(cond.Group); stmt != nil { - return stmt +// bodyHasStatements reports whether a constraint body's members include a +// statement — only then are the body's attribute and kind-less declarations +// steps interleaved with them; a body of declarations alone declares features +// of the check, read as they always were. +func bodyHasStatements(members []ast.Node) bool { + for _, member := range members { + if _, ok := statementKeyword(unwrapBodyMember(member)); ok { + return true } } - return nil + return false +} + +// unwrapBodyMember is the element a membership carries, else the node itself. +func unwrapBodyMember(node ast.Node) ast.Node { + if m, ok := node.(*ast.Membership); ok && m.Member != nil { + return m.Member + } + return node +} + +// constraintBodyStep lowers a member of a constraint body to the step it +// performs, once per member node over the run's model; false for the members +// stating conditions instead (see lower.ConstraintStep). +func (ctx *Context) constraintBodyStep(node ast.Node, scope *symbols.Scope) (lower.Statement, bool) { + if ctx.model.constraintSteps == nil { + ctx.model.constraintSteps = make(map[ast.Node]lower.Statement) + } + if stmt, ok := ctx.model.constraintSteps[node]; ok { + return stmt, true + } + stmt, ok := lower.ConstraintStep(node, scope) + if !ok { + return nil, false + } + ctx.model.constraintSteps[node] = stmt + return stmt, true } // statementKeyword names the keyword a body item the evaluator does not run @@ -440,13 +570,6 @@ func (ctx *Context) evaluateConditions(check conditionCheck, conds []Condition) if len(conds) == 0 { return false, fmt.Errorf("%s %s: %w", check.kind, check.name(), ErrNoConditions) } - // A statement anywhere in the body could change what the conditions read, so - // no verdict is reached, not even from a condition stated before it. - if stmt := unexecutedStatement(conds); stmt != nil { - keyword, _ := statementKeyword(stmt) - return false, fmt.Errorf("%s %s: %s evaluation failed: `%s` %w; bind the value as a feature value or compute it in a calc the condition reads", - check.kind, check.name(), check.what, keyword, ErrStatementNotExecuted) - } if conflict := conflictingResultExpression(conds); conflict != nil { return false, fmt.Errorf("%s %s: %s evaluation failed: %s: %w; a redefinition keeps the inherited condition and tightens it with a nested `assert constraint { … }`", check.kind, check.name(), check.what, conflictText(conflict), ErrConflictingResultExpressions) @@ -460,6 +583,18 @@ func (ctx *Context) evaluateConditions(check conditionCheck, conds []Condition) required := false for _, cond := range conds { required = required || cond.Required + if cond.Steps != nil && !cond.Negated { + // The body's steps ran once; each condition it left is judged on its + // own Required, as the conditions of a body stating no steps are. + done, err := ctx.evaluateStepsConditions(activation, check, cond, features, self) + if err != nil { + return false, err + } + if done { + return true, nil + } + continue + } holds, err := ctx.conditionHolds(activation, cond, features, self, check.frames, check.bindings) if err != nil { return false, fmt.Errorf("%s %s: %s evaluation failed: %w", check.kind, check.name(), check.what, err) @@ -484,6 +619,40 @@ func (ctx *Context) evaluateConditions(check conditionCheck, conds []Condition) return true, nil } +// evaluateStepsConditions is the loop of evaluateConditions for one body that +// states steps: the steps run once against the chain's features and bindings, +// then each condition of the body is evaluated in the frame they left — judged +// on its own Required, so an assumption failing among them still denies +// nothing. done reports a verdict reached early, which happens only for a +// negated element, where one required condition failing is the verdict. +func (ctx *Context) evaluateStepsConditions(activation int64, check conditionCheck, cond Condition, features map[string]scopedExpr, self *Instance) (done bool, err error) { + scoped, bindings := features, check.bindings + for _, constraint := range cond.Constraints { + scoped, bindings = ctx.constraintScope(scoped, bindings, constraint) + } + stepFrame, err := ctx.runConstraintSteps(cond.Steps, scoped, self, check.frames, bindings) + if err != nil { + return false, fmt.Errorf("%s %s: %s evaluation failed: %w", check.kind, check.name(), check.what, err) + } + if len(cond.Group) == 0 { + return false, fmt.Errorf("%s %s: %s evaluation failed: %w: the body's steps leave it no result expression to evaluate", + check.kind, check.name(), check.what, ErrNoConditions) + } + for _, sub := range cond.Group { + holds, err := ctx.conditionHoldsAt(activation, sub, scoped, self, check.frames, bindings, &stepFrame) + if err != nil { + return false, fmt.Errorf("%s %s: %s evaluation failed: %w", check.kind, check.name(), check.what, err) + } + if sub.Required && !holds { + if check.negated { + return true, nil + } + return false, &ViolationError{Kind: check.kind, Element: check.name(), What: check.what, Condition: conditionLabel(sub)} + } + } + return false, nil +} + // conditionSubject is the object a check is about: the one supplied when it // carries the checked element, else the single object of this runtime that does. // A nested object counts, since a redefinition on an object gives a nested @@ -712,18 +881,18 @@ type heldObject struct { // materializing a lazy one as reading its feature value does. A feature value that cannot be read // yields no object: one that is not there is no subject either. func (ctx *Context) nestedObjects(inst *Instance) []heldObject { - out, _ := ctx.heldObjectsOf(inst, nil, false) + out, _ := ctx.heldObjectsOf(inst, nil, false, holdsObjects) return out } // heldObjectsOf is nestedObjects reading a feature as `through` reads it (every one, when nil), // taking what a feature it leaves unread already holds; a failed read is skipped or, where every // object counts, is the error. -func (ctx *Context) heldObjectsOf(inst *Instance, through func(*Instance, ObjectFeature) (*FeatureValue, error), everyObject bool) ([]heldObject, error) { +func (ctx *Context) heldObjectsOf(inst *Instance, through func(*Instance, ObjectFeature) (*FeatureValue, error), everyObject bool, holds func(*EffectiveFeature) bool) ([]heldObject, error) { var out []heldObject read := map[*FeatureValue]bool{} for _, of := range ctx.FeaturesOfObject(inst) { - if of.Name == "" || !holdsObjects(of.Feature) { + if of.Name == "" || !holds(of.Feature) { continue } var fv *FeatureValue @@ -891,13 +1060,36 @@ func (ctx *Context) definitionOf(sym *symbols.Symbol) *symbols.Symbol { // conditionHolds evaluates one condition: an expression, or a group that holds // when all of its conditions hold. Its negation, if any, is applied last. func (ctx *Context) conditionHolds(activation int64, cond Condition, features map[string]scopedExpr, self *Instance, frames []frame, bindings frame) (bool, error) { + return ctx.conditionHoldsAt(activation, cond, features, self, frames, bindings, nil) +} + +// conditionHoldsAt is conditionHolds evaluated in the state stepFrame left when +// the body's statements ran — innermost of the frames read. A condition stating +// steps runs them first, replacing stepFrame for the group that follows them. +func (ctx *Context) conditionHoldsAt(activation int64, cond Condition, features map[string]scopedExpr, self *Instance, frames []frame, bindings frame, stepFrame *frame) (bool, error) { + if stepFrame != nil && (len(cond.Constraints) > 0 || cond.Steps != nil) { + // A nested body is its own performance: the outer body's step frame + // encloses it read-only, shadowed by its bindings and its own steps. + frames = append(append([]frame{}, frames...), *stepFrame) + stepFrame = nil + } for _, constraint := range cond.Constraints { features, bindings = ctx.constraintScope(features, bindings, constraint) } + if cond.Steps != nil { + left, err := ctx.runConstraintSteps(cond.Steps, features, self, frames, bindings) + if err != nil { + return false, err + } + stepFrame = &left + if len(cond.Group) == 0 { + return false, fmt.Errorf("%w: the body's steps leave it no result expression to evaluate", ErrNoConditions) + } + } holds := true if cond.Group != nil { for _, sub := range cond.Group { - subHolds, err := ctx.conditionHolds(activation, sub, features, self, frames, bindings) + subHolds, err := ctx.conditionHoldsAt(activation, sub, features, self, frames, bindings, stepFrame) if err != nil { return false, err } @@ -913,6 +1105,9 @@ func (ctx *Context) conditionHolds(activation int64, cond Condition, features ma if bindings.vars != nil { ec.pushFrame(bindings) } + if stepFrame != nil { + ec.pushFrame(*stepFrame) + } result, err := ec.Eval(cond.Expr) if err != nil { return false, err @@ -1040,9 +1235,19 @@ func unmasked(bindings frame, features map[string]scopedExpr) frame { // conditionLabel renders a condition as written, so a violation names the // condition that failed, negation and grouping included. func conditionLabel(cond Condition) string { - if cond.Statement != nil { - keyword, _ := statementKeyword(cond.Statement) - return "`" + keyword + "` statement" + if cond.Steps != nil { + parts := make([]string, 0, len(cond.Group)) + for _, sub := range cond.Group { + parts = append(parts, conditionLabel(sub)) + } + if len(parts) == 0 { + return "the body's steps" + } + text := "the body's steps then { " + strings.Join(parts, "; ") + " }" + if cond.Negated { + text = "not " + text + } + return text } if cond.Conflict != nil { return "conflicting result expression" diff --git a/internal/exec/runtime/conformance_test.go b/internal/exec/runtime/conformance_test.go index 8c030cec9a..2ebfc35e8a 100644 --- a/internal/exec/runtime/conformance_test.go +++ b/internal/exec/runtime/conformance_test.go @@ -97,6 +97,9 @@ type AdmittedOutcome struct { FinalState string `json:"finalState,omitempty"` Terminated bool `json:"terminated,omitempty"` StateVisits []string `json:"stateVisits,omitempty"` + // Error is the text a run of an action case fails with under the schedules + // reaching this outcome, matched as a substring; it states nothing else. + Error string `json:"error,omitempty"` // Probability states the exact probability from model-weighted draws. Probability *float64 `json:"probability,omitempty"` // ProbabilityRange states the minimum and maximum probability over schedulers. @@ -589,7 +592,7 @@ func exploreConformanceCase(t *testing.T, fresh func() *Context, idx *symbols.In } for _, explored := range exploration.Outcomes { witness := FormatChoices(explored.Witness) - if explored.Outcome.Err != nil { + if explored.Outcome.Err != nil && !listsError(expected.Outcomes) { t.Errorf("exploration reached an error the case does not list: %v\n witness: %s", explored.Outcome.Err, witness) continue } @@ -681,6 +684,9 @@ func conformanceRun(t *testing.T, idx *symbols.Index, path string, expected Expe // validateOutcome checks an outcome a run reached against one the case admits. func validateOutcome(r reporter, ctx *Context, want AdmittedOutcome, got Outcome) { r.Helper() + if validateOutcomeError(r, want.Error, got.Err) { + return + } validateTerminated(r, got.Terminated, want.Terminated) validateFinalState(r, got.FinalState, want.FinalState) validateStateVisits(r, got.StateVisits, want.StateVisits) @@ -689,6 +695,28 @@ func validateOutcome(r reporter, ctx *Context, want AdmittedOutcome, got Outcome } } +// listsError reports whether an admissible set lists a run failing as an outcome. +func listsError(outcomes []AdmittedOutcome) bool { + return slices.ContainsFunc(outcomes, func(o AdmittedOutcome) bool { return o.Error != "" }) +} + +// validateOutcomeError checks a run's failure against an outcome's error, reporting +// whether either states one, so that the outcome's result is not checked besides. +func validateOutcomeError(r reporter, want string, got error) bool { + r.Helper() + switch { + case want == "" && got == nil: + return false + case want == "": + r.Errorf("the run failed with %q, this outcome states a result", got) + case got == nil: + r.Errorf("the run completed, this outcome states the error %q", want) + case !strings.Contains(got.Error(), want): + r.Errorf("the run failed with %q, this outcome states an error containing %q", got, want) + } + return true +} + // oraclePath is the semantic oracle an admissible set must cite a section of. const oraclePath = "../../../docs/project/behavior-semantic-oracle.md" @@ -763,7 +791,15 @@ func admissibleSchemaProblems(expected ExpectedOutcome, oracleTitles map[string] problems = append(problems, "outcomes lists one result; state it as the single outcome") } for i, outcome := range expected.Outcomes { - if outcome.Outputs == nil && outcome.FinalState == "" && outcome.StateVisits == nil && !outcome.Terminated { + if outcome.Error != "" { + if expected.Type != "action" { + problems = append(problems, fmt.Sprintf("outcome %d states an error; error outcomes apply to action cases", i+1)) + } + if outcome.Outputs != nil || outcome.FinalState != "" || outcome.StateVisits != nil || outcome.Terminated || + outcome.Probability != nil || outcome.ProbabilityRange != nil { + problems = append(problems, fmt.Sprintf("outcome %d states an error beside a result", i+1)) + } + } else if outcome.Outputs == nil && outcome.FinalState == "" && outcome.StateVisits == nil && !outcome.Terminated { problems = append(problems, fmt.Sprintf("outcome %d states nothing", i+1)) } if p := outcome.Probability; p != nil && (*p < 0 || *p > 1) { @@ -953,6 +989,12 @@ func runActionConformance(t *testing.T, ctx *Context, idx *symbols.Index, path s // Execute action outcome, err := ctx.ActionOutcomePerformedBy(actionSym, nil, nil) outputs := outcome.Outputs + if err != nil && listsError(expected.Outcomes) { + matchOutcome(t, expected.Outcomes, func(r reporter, admitted AdmittedOutcome) { + validateOutcomeError(r, admitted.Error, err) + }) + return + } if expected.Error != "" { if err == nil { t.Fatalf("expected execution to fail with %q, it completed with outputs %v", expected.Error, outputs) @@ -970,6 +1012,9 @@ func runActionConformance(t *testing.T, ctx *Context, idx *symbols.Index, path s validateOutputs(t, ctx, expected.Outputs, outputs) if len(expected.Outcomes) > 0 { matchOutcome(t, expected.Outcomes, func(r reporter, admitted AdmittedOutcome) { + if validateOutcomeError(r, admitted.Error, nil) { + return + } validateTerminated(r, outcome.Terminated, admitted.Terminated) validateOutputs(r, ctx, admitted.Outputs, outputs) }) diff --git a/internal/exec/runtime/constraint_statements.go b/internal/exec/runtime/constraint_statements.go new file mode 100644 index 0000000000..bcc441f4b4 --- /dev/null +++ b/internal/exec/runtime/constraint_statements.go @@ -0,0 +1,280 @@ +package runtime + +import ( + "fmt" + "sync" + + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// constraintStmtHost runs the steps of one constraint body's Boolean +// performance: it owns its locals and a copy of the constraint's parameters, +// and refuses every effect reaching outside that performance — a write of the +// constrained object's features, a send, a performed action, a terminate — +// and the stated flow a verdict orders by declaration instead. +type constraintStmtHost struct { + ctx *Context + self *Instance + scope *symbols.Scope + steps *BodySteps + orders sync.Map + indexed sync.Once +} + +// describe names the host in a diagnostic. +func (h *constraintStmtHost) describe() string { return "constraint body" } + +// send refuses the message: sending it is an effect outside the performance. +func (h *constraintStmtHost) send(*EvalContext, lower.Send) error { + return fmt.Errorf("%w: %s: a send addresses the world the constraint judges", ErrConstraintEffect, h.describe()) +} + +// declaredOutput reports false: a constraint performance declares no output. +func (h *constraintStmtHost) declaredOutput(string) bool { return false } + +// assignOuter writes a parameter of the constraint into the performance's own +// copy of it (SysML v2 §7.17.9: the implicit target of an assignment is the +// constraint's own performance), and refuses any other name — that names a +// feature of the constrained object or an enclosing frame, not of the +// performance, and writing it is an effect no verdict performs. +func (h *constraintStmtHost) assignOuter(env *stmtEnv, name string, value Value, s lower.Assign) error { + if h.isParameter(name) { + return storeBodyValue(h.ctx, h, env, name, value, s) + } + return fmt.Errorf("%w: %s: the implicit target of an assignment is the constraint's own performance (SysML v2 §7.17.9) and %s is not one of its features", + ErrConstraintExternalAssignment, h.describe(), name) +} + +// isParameter reports whether name declares a parameter the body's own scope +// holds (`in x`) — the features the constraint's performance owns. +func (h *constraintStmtHost) isParameter(name string) bool { + if h.scope == nil { + return false + } + sym, ok := h.scope.LookupLocal(name) + if !ok || sym == nil { + return false + } + u, ok := sym.Decl.(*ast.Usage) + return ok && (u.Direction == ast.DirIn || u.Direction == ast.DirInOut) +} + +// assignData writes a name the performance already holds — a local or a +// parameter copy — checked against its declaration. +func (h *constraintStmtHost) assignData(env *stmtEnv, name string, value Value, s lower.Assign) error { + return storeBodyValue(h.ctx, h, env, name, value, s) +} + +// assignChain refuses a chained target: it writes a feature of the object the +// chain reaches, an effect outside the performance. +func (h *constraintStmtHost) assignChain(_ *EvalContext, s lower.Assign, _ Value) error { + return fmt.Errorf("%w: %s: %s writes a feature of an object outside the constraint's own performance", + ErrConstraintEffect, h.describe(), s.Chain.Text) +} + +// assignForeign refuses a qualified target for the same reason a chained one is. +func (h *constraintStmtHost) assignForeign(_ *EvalContext, s lower.Assign, _ Value) error { + return fmt.Errorf("%w: %s: %s::%s writes a feature of an object outside the constraint's own performance", + ErrConstraintEffect, h.describe(), s.Owner.Name, s.Target) +} + +// acceptReturn refuses a `return`: a verdict is no caller a value returns to. +func (h *constraintStmtHost) acceptReturn(Value, lower.Return) error { + return fmt.Errorf("%w: %s: `return` answers a caller, which a constraint body's verdict has none of", + ErrStatementNotExecutable, h.describe()) +} + +// effect refuses what performs on the world outside the performance — an +// action performed, an accept, a terminate — which a verdict does not perform. +func (h *constraintStmtHost) effect(_ *stmtEngine, s lower.Effect) error { + return fmt.Errorf("%w: %s: the `%s` statement acts outside the constraint's own performance", + ErrConstraintEffect, h.describe(), s.Kind) +} + +// performNode runs a nested action of a block in a frame of the body's, as a +// calculation body's host does; one performing an action of its own acts +// outside the performance, and one stating a flow or a pin connection is the +// stated flow a verdict does not order. +func (h *constraintStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, node *ast.Usage) (stmtFlow, error) { + if _, performs := nestedInvocation(node); performs { + return flowNext, fmt.Errorf("%w: %s: performing action %s is an effect outside the constraint's own performance", + ErrConstraintEffect, h.describe(), ActionNodeName(node)) + } + if connectsPins(graph, node) { + return flowNext, fmt.Errorf("%s: a binding or flow at a pin of %s in a body is not executable", + h.describe(), nodeDescription(node)) + } + var nestedSteps []lower.Statement + var nestedOrder *lower.StatementOrder + if sub, owns := graph.Subflows[node]; owns && sub != nil { + var simple bool + nestedSteps, simple = sub.Graph.StatementList() + if !simple { + return flowNext, fmt.Errorf("%w: %s: the flow %s states of its own in a body is not executable", + ErrStatementNotExecutable, h.describe(), nodeDescription(node)) + } + nestedOrder = graph.StatementOrders[node] + } + engine.env.enter() + defer engine.env.leave() + defer engine.enterActivation()() + for _, feature := range graph.Features[node] { + if feature.Value == nil { + engine.env.declareUnvalued(feature.Name) + continue + } + value, err := engine.evalIn(feature.Scope).Eval(feature.Value) + if err != nil { + return flowNext, fmt.Errorf("eval %s of %s: %w", feature.Name, nodeDescription(node), err) + } + engine.env.declare(feature.Name, value) + } + if nestedSteps != nil { + return engine.runWithOrder(nestedSteps, nestedOrder) + } + return engine.run(graph.Bodies[node]) +} + +// runBlockFlow refuses the flow a loop or branch body states: a verdict orders +// its steps by declaration, not by the successions stated. +func (h *constraintStmtHost) runBlockFlow(engine *stmtEngine, block lower.Block) (stmtFlow, error) { + if block.Stated { + if steps, ok := block.Graph.StatementList(); ok { + return engine.runWithOrder(steps, block.Order) + } + } + return flowNext, fmt.Errorf("%w: %s: the flow a body states in a constraint is not executable", + ErrStatementNotExecutable, h.describe()) +} + +// runFlow refuses the stated flow of a constraint body for the same reason. +func (h *constraintStmtHost) runFlow(block lower.Block) (stmtFlow, error) { + return flowNext, fmt.Errorf("%w: %s: a flow of steps in a body is not executable", + ErrStatementNotExecutable, h.describe()) +} + +// performer is the object the constraint is checked on: what the body's names +// read through, and never write — an assignment's implicit target is the +// performance itself. +func (h *constraintStmtHost) performer() *Instance { return h.self } + +// occurrence is nil: a constraint's verdict materializes no occurrence for `this`. +func (h *constraintStmtHost) occurrence() *Instance { return nil } + +// materializeOccurrence is nil for the same reason occurrence is. +func (h *constraintStmtHost) materializeOccurrence() (*Instance, error) { return nil, nil } + +// runConstraintSteps runs the body's steps as one performance in a fresh frame: +// the check's frames and bindings enclose it, the constraint's parameters are +// copied into it as the steps write them, and nothing is written back — into +// the constrained object, the caller's bindings, or an enclosing frame. It +// answers the frame the steps left, which the body's conditions read innermost. +func (ctx *Context) runConstraintSteps(steps *BodySteps, features map[string]scopedExpr, self *Instance, frames []frame, bindings frame) (frame, error) { + if ctx.statementOrderGuard && steps != nil { + for _, write := range steps.Footprint.Writes { + if !write.Local { + return frame{}, fmt.Errorf("%w: verdict of %s: constraint body may write %s outside its performance", + ErrOrderDependentGuardEffect, ctx.statementOrderGuardLabel, write.String()) + } + } + } + data := frame{vars: make(map[string]Value), unvalued: make(map[string]bool)} + enclosing := make([]frame, 0, len(frames)+1) + enclosing = append(enclosing, frames...) + if bindings.vars != nil { + enclosing = append(enclosing, bindings) + } + host := &constraintStmtHost{ctx: ctx, self: self, scope: steps.Scope, steps: steps} + _, err := ctx.runStatements(func() *stmtEngine { + engine := newStmtEngineIn(ctx, host, data, enclosing) + engine.features = features + return engine + }, steps.Stmts) + if err != nil { + return frame{}, err + } + return data, nil +} + +// statementOrder returns the lowered order when scheduling or precedence needs it. +func (h *constraintStmtHost) statementOrder(stmts []lower.Statement) *lower.StatementOrder { + if h.steps != nil { + h.indexed.Do(func() { + if len(h.steps.Stmts) > 0 && h.steps.Order != nil { + h.orders.Store(&h.steps.Stmts[0], h.steps.Order) + } + indexConstraintStatementOrders(&h.orders, h.steps.Stmts) + }) + } + if len(stmts) < 2 { + return nil + } + key := &stmts[0] + if order, ok := h.orders.Load(key); ok { + order := order.(*lower.StatementOrder) + if h.ctx.scheduling().ordersStatements() || order.HasReversePrecedence() || order.HasSkipped() { + return order + } + return nil + } + order := lower.ConstraintBodyStatementOrder(h.scope, stmts) + actual, _ := h.orders.LoadOrStore(key, order) + order = actual.(*lower.StatementOrder) + if h.ctx.scheduling().ordersStatements() || order.HasReversePrecedence() || order.HasSkipped() { + return order + } + return nil +} + +func indexConstraintStatementOrders(orders *sync.Map, stmts []lower.Statement) { + for _, stmt := range stmts { + switch nested := stmt.(type) { + case lower.If: + indexConstraintBlockOrder(orders, nested.Then) + if nested.Else != nil { + indexConstraintBlockOrder(orders, *nested.Else) + } + case lower.Loop: + indexConstraintBlockOrder(orders, nested.Body) + case lower.Block: + indexConstraintBlockOrder(orders, nested) + } + } +} + +func indexConstraintBlockOrder(orders *sync.Map, block lower.Block) { + if len(block.Statements) > 0 && block.Order != nil { + orders.Store(&block.Statements[0], block.Order) + } + if block.Graph != nil { + indexConstraintGraphOrders(orders, block.Graph) + } + indexConstraintStatementOrders(orders, block.Statements) +} + +func indexConstraintGraphOrders(orders *sync.Map, graph *lower.ActionGraph) { + if graph == nil { + return + } + for node, order := range graph.StatementOrders { + stmts := graph.Bodies[node] + if order != nil { + orders.Store(node, order) + } + if len(stmts) > 0 && order != nil { + orders.Store(&stmts[0], order) + } + indexConstraintStatementOrders(orders, stmts) + } + for _, subflow := range graph.Subflows { + if subflow != nil { + indexConstraintGraphOrders(orders, subflow.Graph) + } + } +} + +func (h *constraintStmtHost) orderStep() int { return h.ctx.enclosingExecutorStep() } + +func (h *constraintStmtHost) yieldsBetweenStatements() bool { return false } diff --git a/internal/exec/runtime/constraint_test.go b/internal/exec/runtime/constraint_test.go index 88d5c317b6..19d92cd5c5 100644 --- a/internal/exec/runtime/constraint_test.go +++ b/internal/exec/runtime/constraint_test.go @@ -237,9 +237,11 @@ func TestConstraintWithoutConditionsIsNotAVerdict(t *testing.T) { } } -func TestConstraintBodyStatementIsNotAVerdict(t *testing.T) { - // The assignment would make the condition hold; ignoring it would report a - // false verdict, so the check must refuse instead. +func TestConstraintBodyStepsRun(t *testing.T) { + // The body's statements are steps of one performance, run before its + // conditions are evaluated: the assignment makes the condition hold. A + // write of the constrained part's feature is refused — the implicit target + // of an assignment is the constraint's own performance (SysML v2 §7.17.9). src := ` package test { constraint def Reassigned { @@ -268,58 +270,56 @@ func TestConstraintBodyStatementIsNotAVerdict(t *testing.T) { t.Fatal("Reassigned not found") } satisfied, err := ctx.EvaluateConstraint(reassigned, testPkg) - if !errors.Is(err, ErrStatementNotExecuted) { - t.Fatalf("err = %v, want ErrStatementNotExecuted", err) - } - if want := "`assign` statement"; err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("err = %v, want it to name the %s", err, want) + if err != nil { + t.Fatalf("err = %v", err) } - if satisfied { - t.Error("a constraint whose body statement was skipped reported as satisfied") + if !satisfied { + t.Error("a constraint whose steps make its condition hold is not satisfied") } rig, ok := testPkg.LookupLocal("Rig") if !ok { t.Fatal("Rig not found") } - feat := featureNamed(ctx, rig, "branched") - if feat == nil || feat.Symbol == nil { - t.Fatal("constraint feature not found") - } - satisfied, err = ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil) - if !errors.Is(err, ErrStatementNotExecuted) { - t.Fatalf("err = %v, want ErrStatementNotExecuted", err) - } - if want := "`if` statement"; err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("err = %v, want it to name the %s", err, want) - } - if satisfied { - t.Error("a constraint whose body statement was skipped reported as satisfied") - } - - // A condition failing before the statement is no verdict either, not even - // for a negated constraint; the group case nests the statement. - for _, name := range []string{"failedFirst", "denied", "grouped"} { + for _, name := range []string{"branched", "failedFirst", "denied"} { feat := featureNamed(ctx, rig, name) if feat == nil || feat.Symbol == nil { t.Fatalf("constraint %s not found", name) } satisfied, err := ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil) - if !errors.Is(err, ErrStatementNotExecuted) { - t.Errorf("%s: err = %v, want ErrStatementNotExecuted", name, err) + if !errors.Is(err, ErrConstraintExternalAssignment) { + t.Errorf("%s: err = %v, want ErrConstraintExternalAssignment", name, err) } - if want := "`assign` statement"; err == nil || !strings.Contains(err.Error(), want) { - t.Errorf("%s: err = %v, want it to name the %s", name, err, want) + if want := "z is not one of its features"; err == nil || !strings.Contains(err.Error(), want) { + t.Errorf("%s: err = %v, want it to say %s", name, err, want) } if satisfied { - t.Errorf("%s: reported as satisfied with its body statement skipped", name) + t.Errorf("%s: reported as satisfied with its refused write", name) } } + + // A required condition failing first reports the violation: the nested + // body's steps are never reached. + feat := featureNamed(ctx, rig, "grouped") + if feat == nil || feat.Symbol == nil { + t.Fatal("constraint grouped not found") + } + satisfied, err = ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil) + var violation *ViolationError + if !errors.As(err, &violation) { + t.Fatalf("grouped: err = %v, want a *ViolationError", err) + } + if violation.Condition != "z > 100" { + t.Errorf("grouped: violated condition = %q, want z > 100", violation.Condition) + } + if satisfied { + t.Error("grouped: reported as satisfied") + } } -func TestConstraintBodyPerformIsNotAVerdict(t *testing.T) { - // A performed action is a usage, not a statement node, and it is one more - // thing the body does that a verdict would have to account for. +func TestConstraintBodyPerformIsRefused(t *testing.T) { + // A performed action is a usage, not a statement node, and it is an effect + // outside the constraint's own performance, which a verdict refuses. src := ` package test { action def Bump { inout n; assign n := n + 10; } @@ -349,8 +349,8 @@ func TestConstraintBodyPerformIsNotAVerdict(t *testing.T) { t.Fatal("Performed not found") } satisfied, err := ctx.EvaluateConstraint(performed, testPkg) - if !errors.Is(err, ErrStatementNotExecuted) { - t.Fatalf("err = %v, want ErrStatementNotExecuted", err) + if !errors.Is(err, ErrConstraintEffect) { + t.Fatalf("err = %v, want ErrConstraintEffect", err) } if want := "`perform` statement"; err == nil || !strings.Contains(err.Error(), want) { t.Errorf("err = %v, want it to name the %s", err, want) @@ -373,8 +373,8 @@ func TestConstraintBodyPerformIsNotAVerdict(t *testing.T) { evaluate = ctx.EvaluateRequirementOn } satisfied, err := evaluate(feat.Symbol, feat.DeclScope(), nil) - if !errors.Is(err, ErrStatementNotExecuted) { - t.Errorf("%s: err = %v, want ErrStatementNotExecuted", name, err) + if !errors.Is(err, ErrConstraintEffect) { + t.Errorf("%s: err = %v, want ErrConstraintEffect", name, err) } if want := "`perform` statement"; err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: err = %v, want it to name the %s", name, err, want) @@ -385,9 +385,9 @@ func TestConstraintBodyPerformIsNotAVerdict(t *testing.T) { } } -func TestConstraintBodyActionFlowIsNotAVerdict(t *testing.T) { - // Action nodes and the successions between them are steps of the body too: - // a verdict that skipped them would answer a different constraint. +func TestConstraintBodyActionFlowIsRefused(t *testing.T) { + // Action nodes perform actions — an effect outside the performance — and + // the successions between them are a stated flow a verdict does not order. src := ` package test { constraint def Flowed { @@ -420,10 +420,10 @@ func TestConstraintBodyActionFlowIsNotAVerdict(t *testing.T) { t.Fatal("Flowed not found") } satisfied, err := ctx.EvaluateConstraint(flowed, testPkg) - if !errors.Is(err, ErrStatementNotExecuted) { - t.Fatalf("err = %v, want ErrStatementNotExecuted", err) + if !errors.Is(err, ErrConstraintEffect) { + t.Fatalf("err = %v, want ErrConstraintEffect", err) } - if want := "`action` statement"; err == nil || !strings.Contains(err.Error(), want) { + if want := "`perform` statement"; err == nil || !strings.Contains(err.Error(), want) { t.Errorf("err = %v, want it to name the %s", err, want) } if satisfied { @@ -444,10 +444,10 @@ func TestConstraintBodyActionFlowIsNotAVerdict(t *testing.T) { evaluate = ctx.EvaluateRequirementOn } satisfied, err := evaluate(feat.Symbol, feat.DeclScope(), nil) - if !errors.Is(err, ErrStatementNotExecuted) { - t.Errorf("%s: err = %v, want ErrStatementNotExecuted", name, err) + if !errors.Is(err, ErrConstraintEffect) { + t.Errorf("%s: err = %v, want ErrConstraintEffect", name, err) } - if want := "`action` statement"; err == nil || !strings.Contains(err.Error(), want) { + if want := "`perform` statement"; err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: err = %v, want it to name the %s", name, err, want) } if satisfied { @@ -456,9 +456,9 @@ func TestConstraintBodyActionFlowIsNotAVerdict(t *testing.T) { } } -func TestConstraintBodySuccessionAloneIsNotAVerdict(t *testing.T) { - // A succession between actions declared outside the body is still a step - // the body states, and it is named by the keyword it was written with. +func TestConstraintBodySuccessionIsRefused(t *testing.T) { + // A succession between actions declared outside the body is a stated flow + // a verdict does not order, and it is named by the keyword written. src := ` package test { part def Rig { @@ -495,8 +495,8 @@ func TestConstraintBodySuccessionAloneIsNotAVerdict(t *testing.T) { t.Fatalf("%s not found", name) } satisfied, err := ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil) - if !errors.Is(err, ErrStatementNotExecuted) { - t.Errorf("%s: err = %v, want ErrStatementNotExecuted", name, err) + if !errors.Is(err, ErrStatementNotExecutable) { + t.Errorf("%s: err = %v, want ErrStatementNotExecutable", name, err) } if err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: err = %v, want it to name the %s", name, err, want) diff --git a/internal/exec/runtime/context.go b/internal/exec/runtime/context.go index 61c59464ce..87d115908d 100644 --- a/internal/exec/runtime/context.go +++ b/internal/exec/runtime/context.go @@ -6,6 +6,7 @@ import ( "slices" "strings" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -27,7 +28,9 @@ type Context struct { took *idMark maxSteps int64 instances map[int64]*Instance - created []int64 + // flowShares caches, by graph, whether two moves of its flow may touch what another does. + flowShares map[*lower.ActionGraph]bool + created []int64 // lives holds, per registered object, when it began and ended (lifetimes.go). lives map[int64]life // lifetimes stands for the lives as a `=` value reads them, to derive again when they change. @@ -200,6 +203,14 @@ type Context struct { // probes is the number of probes under way; see beginProbe. probes int + // statementOrderSweep resolves statement-order choices locally while a probe + // enumerates the orders it can observe. + statementOrderSweep *statementOrderSweep + statementOrderGuard bool + statementOrderGuardLabel string + statementOrderGuardBodies map[string]bool + invocationOrderMemo *invocationOrderMemo + orderAnalysis map[*symbols.Symbol]*bodyOrderAnalysis // journals is the number of probes and transactions under way: while one is, // every change is journaled for it to undo; see beginJournal. journals int @@ -296,6 +307,16 @@ type Context struct { // readingSubsetted holds the optional features whose subsetted collections are // being read ahead of them, so two subsetting each other do not recurse. readingSubsetted map[featureValueRef]bool + + // resolvingNamespaceClasses holds the namespace binding classes being resolved, so + // a member read while one is under way resolves as usual rather than recursing. + resolvingNamespaceClasses map[*namespaceClass]bool + // namespaceChainReads holds the classes whose chain ends are being evaluated: + // such a read of a member of the same class is a cyclic dependency. + namespaceChainReads map[*namespaceClass]bool + // namespaceCollecting holds the namespace usages whose subsetting usages are + // being read, so two subsetting each other are reported as a cycle. + namespaceCollecting map[*symbols.Symbol]bool } // featureValueRef identifies one feature value of one instance. @@ -372,6 +393,10 @@ func NewContext(model *Model, maxSteps int64) *Context { readingSubsetted: make(map[featureValueRef]bool), successionOrderNotes: make(map[successionOrderNoteKey]bool), + resolvingNamespaceClasses: make(map[*namespaceClass]bool), + namespaceChainReads: make(map[*namespaceClass]bool), + namespaceCollecting: make(map[*symbols.Symbol]bool), + shareDefaults: SharedDefaultsFromEnv(), sharedDefaults: make(map[sharedKey]*sharedDefault), shapes: make(map[shapeNode]*shapeNode), diff --git a/internal/exec/runtime/errors.go b/internal/exec/runtime/errors.go index 2e4d85e172..0d2a8934f6 100644 --- a/internal/exec/runtime/errors.go +++ b/internal/exec/runtime/errors.go @@ -339,9 +339,24 @@ var ( // condition to evaluate: reporting a verdict would claim a check that never ran. ErrNoConditions = errors.New("no condition to evaluate") - // ErrStatementNotExecuted is returned when a constraint body states an action - // statement: the evaluator does not run it, so a verdict would ignore it. - ErrStatementNotExecuted = errors.New("statement in a constraint body is not executed by OpenSysML") + // ErrConstraintExternalAssignment is returned when a constraint body's + // assignment targets a name the constraint's own performance holds no + // feature for — the implicit target is the performance (SysML v2 §7.17.9), + // and the name is a feature of the constrained object, not of it. + ErrConstraintExternalAssignment = errors.New("assignment outside the constraint performance") + + // ErrConstraintEffect is returned when a constraint body states an effect + // outside its own performance — a chained or qualified write, a send, a + // performed action, a terminate — which a verdict does not perform. + ErrConstraintEffect = errors.New("effect outside the constraint performance") + + // ErrOrderDependentPreview is returned when a guard's verdict varies by + // statement order inside a preview, where a scheduler choice cannot be made. + ErrOrderDependentPreview = errors.New("order-dependent guard in a preview") + + // ErrOrderDependentGuardEffect is returned when a swept guard constraint may + // write a feature outside its own performance. + ErrOrderDependentGuardEffect = errors.New("order-dependent guard may write outside its constraint performance") // ErrUnboundSubject is returned when a condition reads a subject nothing // supplied: the check is about no object, so it reaches no verdict. diff --git a/internal/exec/runtime/eval.go b/internal/exec/runtime/eval.go index e71db8c3eb..7ee26c099d 100644 --- a/internal/exec/runtime/eval.go +++ b/internal/exec/runtime/eval.go @@ -454,6 +454,13 @@ func (ctx *Context) EvalDeclaredValue(sym *symbols.Symbol) (Value, error) { if ctx.namesOneObject(sym) || ctx.namesObjects(sym) { return ctx.denotedValue(sym) } + // A usage a binding connector governs reads as the binding's value, as + // an expression read of the same name answers. + if class, _ := ctx.namespaceClassMember(sym); class != nil || ctx.optionalValueless(sym) { + if val, bound, err := ctx.namespaceBoundValue(sym); bound || err != nil { + return val, err + } + } // Read as a name of it is read: a feature nothing values is undetermined. return NewEvalContext(ctx, sym.OwnerScope).withoutValue(sym, ctx.qualifiedSymbolName(sym), nil) } @@ -1100,6 +1107,22 @@ func (ec *EvalContext) declaredValue(sym *symbols.Symbol, value ast.Node) (Value if !namespaceObjectUsage(sym) { return ec.evaluateDeclared(sym, value) } + // A binding connector joining this usage makes its ends denote the same + // values; resolving it records the binding before the value is read. + if _, bound, err := ec.ctx.namespaceBoundObjects(sym); err != nil { + return Value{}, err + } else if bound { + if val, ok := ec.ctx.namespaceBindings[sym]; ok { + return val, nil + } + // The class's member declaring sym may be a different scope tree's + // symbol for it; its recorded binding is this usage's value too. + if _, member := ec.ctx.namespaceClassMember(sym); member != sym { + if val, ok := ec.ctx.namespaceBindings[member]; ok { + return val, nil + } + } + } if ec.ctx.binding(sym) { return Value{}, &CyclicBindingError{Usage: sym, Stated: ec.ctx.qualifiedSymbolName(sym)} } @@ -1178,7 +1201,15 @@ func (ec *EvalContext) conformHeld(sym *symbols.Symbol, val Value, countJudged b // materialized once. Reports whether the symbol denotes such objects. func (ec *EvalContext) occurrenceReference(sym *symbols.Symbol) (Value, bool, error) { if !ec.ctx.namesOneObject(sym) && !ec.ctx.namesObjects(sym) { - return Value{}, false, nil + class, _ := ec.ctx.namespaceClassMember(sym) + if class == nil && !ec.ctx.optionalValueless(sym) { + return Value{}, false, nil + } + // Of itself the usage may denote nothing, but a binding connector + // may have bound it to another usage's value, or a subsetting may + // have filled it — reads through the binding answer those. + ec.ctx.noteDeclarationRead(sym) + return ec.ctx.namespaceBoundValue(sym) } ec.ctx.noteDeclarationRead(sym) val, err := ec.ctx.denotedValue(sym) diff --git a/internal/exec/runtime/explore.go b/internal/exec/runtime/explore.go index b84f97d174..7fe74c4571 100644 --- a/internal/exec/runtime/explore.go +++ b/internal/exec/runtime/explore.go @@ -92,6 +92,16 @@ func (c ChoiceTaken) String() string { return fmt.Sprintf("step %d: %s -> %s%s", c.Step, choiceLabel(c.Where), choiceLabel(c.Took), c.weightedTail()) case ChoiceTransition: return fmt.Sprintf("%s -> %s%s", choiceLabel(c.Where), choiceLabel(c.Took), c.weightedTail()) + case ChoiceStatementOrder: + if c.Step > 0 { + return fmt.Sprintf("step %d: %s: %s first of %s", c.Step, choiceLabel(c.Where), choiceLabel(c.Took), choiceLabels(c.Among)) + } + return fmt.Sprintf("%s: %s first of %s", choiceLabel(c.Where), choiceLabel(c.Took), choiceLabels(c.Among)) + case ChoiceGuardOrder: + if c.Step > 0 { + return fmt.Sprintf("step %d: %s: %s first of %s", c.Step, choiceLabel(c.Where), choiceLabel(c.Took), choiceLabels(c.Among)) + } + return fmt.Sprintf("%s: %s first of %s", choiceLabel(c.Where), choiceLabel(c.Took), choiceLabels(c.Among)) case ChoiceRegionOrder, ChoiceDueOrder, ChoiceDispatchOrder, ChoiceEntryOrder, ChoiceExitOrder, ChoiceStepOrder, ChoiceEntryStep: return fmt.Sprintf("%s: %s first of %s", choiceLabel(c.Where), choiceLabel(c.Took), choiceLabels(c.Among)) } @@ -198,6 +208,8 @@ func (x *Exploration) Status() string { limit := x.Budget.Runs if budget == "depth" { limit = x.Budget.Depth + } else if budget == BoundStatementOrders { + limit = maxStatementOrderEvaluations } named[i] = fmt.Sprintf("%s budget %d", budget, limit) } diff --git a/internal/exec/runtime/explore_queue.go b/internal/exec/runtime/explore_queue.go index ea8424d5f3..3d64450916 100644 --- a/internal/exec/runtime/explore_queue.go +++ b/internal/exec/runtime/explore_queue.go @@ -305,6 +305,10 @@ func (q *exploreQueue) fold() { return } q.depthHit = q.depthHit || p.replay.depthHit + if errors.Is(p.outcome.Err, ErrStatementOrderSweepLimit) && + !slices.Contains(q.result.BudgetsHit, BoundStatementOrders) { + q.result.BudgetsHit = append(q.result.BudgetsHit, BoundStatementOrders) + } q.result.Scope = scopeWith(q.result.Scope, p.outcome.Scope) if i, seen := q.reached[p.identity]; !seen { q.reached[p.identity] = len(q.result.Outcomes) diff --git a/internal/exec/runtime/explore_test.go b/internal/exec/runtime/explore_test.go index ef34c8198f..934c0f074d 100644 --- a/internal/exec/runtime/explore_test.go +++ b/internal/exec/runtime/explore_test.go @@ -1043,7 +1043,7 @@ func TestExploreDueOrder(t *testing.T) { state waiting; accept after 5 [s] then took; state took { - entry action take { assign seen := cell.mark; assign cell.mark := cell.mark + 1; } + entry action take { assign seen := cell.mark; then assign cell.mark := cell.mark + 1; } } } state a : Ticker; diff --git a/internal/exec/runtime/extent.go b/internal/exec/runtime/extent.go index 19bd880500..348f6619f0 100644 --- a/internal/exec/runtime/extent.go +++ b/internal/exec/runtime/extent.go @@ -102,7 +102,7 @@ func (ctx *Context) objectsOf(roots []*Instance, target *symbols.Symbol) (Value, seen := make(map[int64]bool) path := make(map[*symbols.Symbol]int) through := func(inst *Instance, of ObjectFeature) (*FeatureValue, error) { - if !ctx.mayHold(of.Feature.Symbol, target, make(map[*symbols.Symbol]bool)) { + if !ctx.extentHeldMember(of.Feature.Symbol) || !ctx.mayHold(of.Feature.Symbol, target, make(map[*symbols.Symbol]bool)) { return nil, nil } // A value whose every possible type is on the path is not read; any other read tells by what it made. @@ -144,7 +144,7 @@ func (ctx *Context) objectsOf(roots []*Instance, target *symbols.Symbol) (Value, path[decl]-- } }() - children, err := ctx.heldObjectsOf(inst, through, true) + children, err := ctx.heldObjectsOf(inst, through, true, extentHoldsObjects) if err != nil { return fmt.Errorf("object of %s: %w", symbolText(inst.Type), err) } @@ -205,7 +205,7 @@ func (ctx *Context) mayReach(inst *Instance, target *symbols.Symbol) bool { return true } for _, of := range ctx.FeaturesOfObject(inst) { - if of.Name != "" && holdsObjects(of.Feature) && ctx.mayHold(of.Feature.Symbol, target, make(map[*symbols.Symbol]bool)) { + if of.Name != "" && ctx.extentHeldMember(of.Feature.Symbol) && ctx.mayHold(of.Feature.Symbol, target, make(map[*symbols.Symbol]bool)) { return true } } @@ -507,13 +507,46 @@ func (ctx *Context) mayHold(typ, target *symbols.Symbol, visited map[*symbols.Sy } } for _, member := range ctx.model.semantics.MembersOf(typ) { - if objectFeature(member) && ctx.mayHold(member, target, visited) { + if ctx.extentHeldMember(member) && ctx.mayHold(member, target, visited) { return true } } return false } +// extentHeldMember is extentHeldFeature for a member of a type under the walk: +// a performance member of a library type does not make its holder a candidate — +// the library objects a run keeps are not the extent's. +func (ctx *Context) extentHeldMember(sym *symbols.Symbol) bool { + return extentHeldFeature(sym) && (objectFeature(sym) || !ctx.libraryDeclared(sym)) +} + +// extentHeldFeature reports whether an extent's descent reads a feature's held +// objects: the object features objectFeature admits, and the occurrences an +// object performs — actions, states, connections, interfaces, allocations and +// flows — which `all T` reaches though reading them may start their behaviors. +func extentHeldFeature(sym *symbols.Symbol) bool { + if objectFeature(sym) { + return true + } + usage, ok := sym.Decl.(*ast.Usage) + if !ok { + return false + } + switch usage.Kind { + case ast.UsageAction, ast.UsageState, ast.UsageConnection, ast.UsageInterface, + ast.UsageAllocation, ast.UsageFlow: + return true + } + return false +} + +// extentHoldsObjects is holdsObjects for the extent walk: the features whose +// held objects `all T` enumerates, performances included. +func extentHoldsObjects(feat *EffectiveFeature) bool { + return extentHeldFeature(feat.Symbol) +} + // isOf reports whether a type inst is of, or was classified by, conforms to target. func (ctx *Context) isOf(inst *Instance, target *symbols.Symbol) bool { for _, typ := range inst.types() { diff --git a/internal/exec/runtime/frame.go b/internal/exec/runtime/frame.go index 298690206e..e955d8fea3 100644 --- a/internal/exec/runtime/frame.go +++ b/internal/exec/runtime/frame.go @@ -10,6 +10,9 @@ import ( type frame struct { slots *slotFrame vars map[string]Value + // unvalued marks names the frame declares but holds no value for: a read + // answers as missing, a write binds. + unvalued map[string]bool // aliases map the name of a redefined feature to the name of the feature // redefining it, which the frame binds it under (`in g :>> x` holds x as g). aliases map[string]string @@ -133,18 +136,31 @@ func (f frame) lookup(name string) (Value, bool) { return value, ok } -// has reports whether the frame binds name. +// has reports whether the frame binds name or declares it unvalued. func (f frame) has(name string) bool { + if f.unvalued[name] { + return true + } _, ok := f.lookup(name) return ok } +// markUnvalued records a name the frame declares but holds no value for, which +// a read answers as missing and a write binds. +func (f frame) markUnvalued(name string) { + if f.unvalued == nil { + f.unvalued = map[string]bool{} + } + f.unvalued[name] = true +} + // set binds name: in its slot when the frame has one for it, else in the map. func (f frame) set(name string, value Value) { name = canonical(f.aliases, name) if f.slots != nil && f.slots.set(name, value) { return } + delete(f.unvalued, name) f.vars[name] = value } diff --git a/internal/exec/runtime/guard_statement_order.go b/internal/exec/runtime/guard_statement_order.go new file mode 100644 index 0000000000..61b866e8b6 --- /dev/null +++ b/internal/exec/runtime/guard_statement_order.go @@ -0,0 +1,290 @@ +package runtime + +import ( + "fmt" + "sort" + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast/astcodec" +) + +const guardOrderWherePrefix = "verdict of " + +type guardOrderResult struct { + holds bool + err error + choices []ChoiceTaken +} + +func (ctx *Context) guardUnderStatementOrders(guard ast.Node, step int, scope *symbols.Scope, eval func() (bool, error)) (bool, error) { + if !ctx.scheduling().ordersStatements() { + return eval() + } + needsSweep, externalWrite := ctx.guardStatementOrderInfo(guard, scope) + if externalWrite != nil { + return false, fmt.Errorf("%w: verdict of %s: constraint body may write %s outside its performance", + ErrOrderDependentGuardEffect, conditionText(guard), externalWrite.String()) + } + if !needsSweep { + return eval() + } + if ctx.statementOrderSweep != nil { + return ctx.evalOrderAwareGuard(guard, eval) + } + + probeDepth := ctx.probes + results := make(map[string]guardOrderResult) + bodyNames := make(map[string]bool) + previousBodies := ctx.statementOrderGuardBodies + previousGuard := ctx.statementOrderGuard + previousLabel := ctx.statementOrderGuardLabel + ctx.statementOrderGuardBodies = bodyNames + ctx.statementOrderGuard = true + ctx.statementOrderGuardLabel = conditionText(guard) + err := ctx.sweepStatementOrderVariants(func(sweep *statementOrderSweep) error { + holds, err := eval() + key := guardOrderResultKey(holds, err) + if _, seen := results[key]; !seen { + results[key] = guardOrderResult{holds: holds, err: err, choices: append([]ChoiceTaken(nil), sweep.choices...)} + } + return nil + }) + ctx.statementOrderGuardBodies = previousBodies + ctx.statementOrderGuard = previousGuard + ctx.statementOrderGuardLabel = previousLabel + if err != nil { + return false, err + } + ordered := guardOrderResults(results) + if len(ordered) == 0 { + return false, nil + } + if len(ordered) == 1 { + return ctx.evalGuardAtOrder(guard, ordered[0].choices, eval) + } + body := guardOrderBodyName(bodyNames) + label := conditionText(guard) + if probeDepth > 0 { + return false, fmt.Errorf("%w: verdict of %s depends on the statement orders of %s", + ErrOrderDependentPreview, label, body) + } + alternatives := make([]string, len(ordered)) + for i, result := range ordered { + switch { + case result.err != nil: + alternatives[i] = "error: " + result.err.Error() + case result.holds: + alternatives[i] = "holds" + default: + alternatives[i] = "does not hold" + } + } + where := fmt.Sprintf("%s%s under the statement orders of %s", guardOrderWherePrefix, label, body) + choice := ChoicePoint{ + Kind: ChoiceGuardOrder, + Step: step, + Where: where, + Alternatives: alternatives, + Span: guard.Span(), + } + if scope != nil { + choice.File = scope.DocName() + } + choice.Taken = ctx.scheduling().choose(choice, nil) + if err := ctx.scheduling().refusal(); err != nil { + return false, err + } + ctx.noteChoice(choice) + return ctx.evalGuardAtOrder(guard, ordered[choice.Taken].choices, eval) +} + +func (ctx *Context) evalGuardAtOrder(guard ast.Node, choices []ChoiceTaken, eval func() (bool, error)) (bool, error) { + prefix := make([]int, len(choices)) + for i, choice := range choices { + prefix[i] = choice.Taken + } + sweep := &statementOrderSweep{prefix: prefix} + previousSweep := ctx.statementOrderSweep + previousGuard := ctx.statementOrderGuard + previousLabel := ctx.statementOrderGuardLabel + previousBodies := ctx.statementOrderGuardBodies + ctx.statementOrderSweep = sweep + ctx.statementOrderGuard = true + ctx.statementOrderGuardLabel = conditionText(guard) + ctx.statementOrderGuardBodies = make(map[string]bool) + defer func() { + ctx.statementOrderSweep = previousSweep + ctx.statementOrderGuard = previousGuard + ctx.statementOrderGuardLabel = previousLabel + ctx.statementOrderGuardBodies = previousBodies + }() + return eval() +} + +func (ctx *Context) evalOrderAwareGuard(guard ast.Node, eval func() (bool, error)) (bool, error) { + previousGuard := ctx.statementOrderGuard + previousLabel := ctx.statementOrderGuardLabel + ctx.statementOrderGuard = true + ctx.statementOrderGuardLabel = conditionText(guard) + defer func() { + ctx.statementOrderGuard = previousGuard + ctx.statementOrderGuardLabel = previousLabel + }() + return eval() +} + +func (ctx *Context) guardStatementOrderInfo(guard ast.Node, scope *symbols.Scope) (bool, *lower.Place) { + if guard == nil || ctx.model == nil || ctx.model.resolver == nil { + return false, nil + } + needsSweep := false + var externalWrite *lower.Place + seen := make(map[*symbols.Symbol]bool) + var inspect func(*symbols.Symbol) + inspect = func(sym *symbols.Symbol) { + if sym == nil || seen[sym] { + return + } + seen[sym] = true + switch sym.Kind { + case symbols.SymbolConstraintDef, symbols.SymbolConstraintUsage, + symbols.SymbolCalcDef, symbols.SymbolCalcUsage: + needsSweep = needsSweep || ctx.reordersTransitively(sym) + if sym.Kind == symbols.SymbolCalcDef || sym.Kind == symbols.SymbolCalcUsage { + return + } + for _, condition := range ctx.ConditionsOf(sym, sym.Scope) { + write := conditionExternalWrite(condition) + if externalWrite == nil && write != nil { + externalWrite = write + } + ctx.inspectConditionCalls(condition, sym.Scope, inspect) + for _, constraint := range condition.Constraints { + inspect(constraint) + } + } + case symbols.SymbolAlias: + if target, ok := ctx.model.resolver.ResolveAliasTarget(sym); ok { + inspect(target) + } + } + } + for node := range astcodec.Reachable(guard) { + switch expression := node.(type) { + case *ast.InvocationExpr: + if expression.Type != nil { + for _, sym := range ctx.model.resolver.InvocationCandidates(scope, expression.Type) { + inspect(sym) + } + } + case *ast.FeatureReference: + if expression.Name != nil { + if sym, ok := ctx.model.resolver.ResolveQualified(scope, expression.Name); ok { + inspect(sym) + } + } + } + } + return needsSweep || externalWrite != nil, externalWrite +} + +func (ctx *Context) inspectConditionCalls(condition Condition, fallback *symbols.Scope, inspect func(*symbols.Symbol)) { + scope := condition.Scope + if scope == nil { + scope = fallback + } + for node := range astcodec.Reachable(condition.Expr) { + switch expression := node.(type) { + case *ast.InvocationExpr: + if expression.Type != nil { + for _, sym := range ctx.model.resolver.InvocationCandidates(scope, expression.Type) { + inspect(sym) + } + } + case *ast.FeatureReference: + if expression.Name != nil { + if sym, ok := ctx.model.resolver.ResolveQualified(scope, expression.Name); ok { + inspect(sym) + } + } + } + } + for _, nested := range condition.Group { + ctx.inspectConditionCalls(nested, scope, inspect) + } +} + +func conditionExternalWrite(condition Condition) *lower.Place { + var externalWrite *lower.Place + if condition.Steps != nil { + for _, write := range condition.Steps.Footprint.Writes { + if !write.Local { + copy := write + externalWrite = © + break + } + } + } + for _, nested := range condition.Group { + write := conditionExternalWrite(nested) + if externalWrite == nil { + externalWrite = write + } + } + return externalWrite +} + +func guardOrderResultKey(holds bool, err error) string { + if err != nil { + return "error:" + err.Error() + } + if holds { + return "holds" + } + return "does not hold" +} + +func guardOrderResults(results map[string]guardOrderResult) []guardOrderResult { + keys := make([]string, 0, len(results)) + for key := range results { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + return guardOrderResultRank(keys[i]) < guardOrderResultRank(keys[j]) || + guardOrderResultRank(keys[i]) == guardOrderResultRank(keys[j]) && keys[i] < keys[j] + }) + ordered := make([]guardOrderResult, len(keys)) + for i, key := range keys { + ordered[i] = results[key] + } + return ordered +} + +func guardOrderResultRank(key string) int { + switch key { + case "holds": + return 0 + case "does not hold": + return 1 + default: + return 2 + } +} + +func guardOrderBodyName(names map[string]bool) string { + ordered := make([]string, 0, len(names)) + for name := range names { + ordered = append(ordered, name) + } + sort.Strings(ordered) + if len(ordered) == 0 { + return "constraint or calculation body" + } + if len(ordered) == 1 { + return ordered[0] + } + return strings.Join(ordered, ", ") +} diff --git a/internal/exec/runtime/instance.go b/internal/exec/runtime/instance.go index 68c18ba227..1607964af7 100644 --- a/internal/exec/runtime/instance.go +++ b/internal/exec/runtime/instance.go @@ -392,8 +392,19 @@ func (ctx *Context) materialize(sym *symbols.Symbol, id int64, owner *Instance, // declared in a package names one occurrence, so reading its features twice // reads the same object. func (ctx *Context) occurrenceOf(sym *symbols.Symbol) (*Instance, error) { - if live, ok := ctx.liveOccurrences(sym); ok && len(live) == 1 { - return live[0], nil + if objs, bound, err := ctx.namespaceBoundObjects(sym); err != nil { + return nil, err + } else if bound && len(objs) == 1 { + return objs[0], nil + } else if bound { + return nil, fmt.Errorf("usage %s: %w: denotes %d occurrences, not one", symbolText(sym), ErrMultiplicityViolation, len(objs)) + } + if objs, subsetted, err := ctx.namespacedSubsetObjects(sym); err != nil { + return nil, err + } else if subsetted && len(objs) == 1 { + return objs[0], nil + } else if subsetted { + return nil, fmt.Errorf("usage %s: %w: denotes %d occurrences, not one", symbolText(sym), ErrMultiplicityViolation, len(objs)) } // The occurrence is recorded before its behaviors start, so a behavior that // reaches the usage it belongs to reads this object rather than a second one. @@ -413,8 +424,15 @@ func (ctx *Context) occurrenceOf(sym *symbols.Symbol) (*Instance, error) { // occurrencesOf returns the objects a namespace-level usage of several occurrences denotes, // materializing its lower bound once, in declaration order, as a nested collection's is. func (ctx *Context) occurrencesOf(sym *symbols.Symbol) ([]*Instance, error) { - if live, ok := ctx.liveOccurrences(sym); ok { - return live, nil + if objs, bound, err := ctx.namespaceBoundObjects(sym); err != nil { + return nil, err + } else if bound { + return objs, nil + } + if objs, subsetted, err := ctx.namespacedSubsetObjects(sym); err != nil { + return nil, err + } else if subsetted { + return objs, nil } count, err := ctx.lowerBoundCount(ctx.featureMultiplicity(sym, ctx.findOwnerType(sym)), 0, symbolText(sym)) if err != nil { @@ -524,6 +542,13 @@ func (ctx *Context) denotedObjects(sym *symbols.Symbol) ([]*Instance, error) { } return members, nil case ctx.optionalValueless(sym): + // Of itself an optional usage denotes nothing, but a binding connector + // may have bound it to another usage's value. + if objs, bound, err := ctx.namespaceBoundObjects(sym); err != nil { + return nil, err + } else if bound { + return objs, nil + } return nil, nil } return nil, ctx.undenotedUsage(sym) @@ -548,7 +573,7 @@ func (ctx *Context) denotedValue(sym *symbols.Symbol) (Value, error) { if tail != nil { held += tail.count } - if mult := ctx.featureMultiplicity(sym, ctx.findOwnerType(sym)); mult.AdmitsMore(held) { + if mult := ctx.featureMultiplicity(sym, ctx.findOwnerType(sym)); mult.AdmitsMore(held) && !symbols.IsAbstract(sym) { spelled := ctx.qualifiedSymbolName(sym) return undeterminedFeatureValue(openCountReason(spelled, mult), mult, sym), nil } diff --git a/internal/exec/runtime/invocation_statement_order.go b/internal/exec/runtime/invocation_statement_order.go new file mode 100644 index 0000000000..0e25f8a1c4 --- /dev/null +++ b/internal/exec/runtime/invocation_statement_order.go @@ -0,0 +1,253 @@ +package runtime + +import ( + "errors" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" +) + +type invocationOrderKey struct { + shape *calcShape + self int64 + arguments string +} + +type invocationOrderResult struct { + value Value + err error + spelling string + key string + prefix []int + first bool +} + +type invocationOrderMemo struct { + results map[invocationOrderKey][]invocationOrderResult + resolving map[invocationOrderKey]bool +} + +const resultOrderWherePrefix = "result of " + +func (ctx *Context) invokeWithStatementOrderResults( + shape *calcShape, + args calcArgs, + self *Instance, + memoize bool, + invoke func() (Value, error), +) (Value, error) { + outermost := ctx.invocationOrderMemo == nil + if outermost { + ctx.invocationOrderMemo = &invocationOrderMemo{ + results: make(map[invocationOrderKey][]invocationOrderResult), + resolving: make(map[invocationOrderKey]bool), + } + } + if outermost { + defer func() { ctx.invocationOrderMemo = nil }() + } + + key := invocationOrderKey{shape: shape, arguments: canonicalInvocationArguments(shape, args)} + if self != nil { + key.self = self.ID + } + memo := ctx.invocationOrderMemo + if memoize && memo.resolving[key] { + if ctx.statementOrderSweep == nil { + return invoke() + } + previous := ctx.statementOrderSweep + ctx.statementOrderSweep = &statementOrderSweep{} + defer func() { ctx.statementOrderSweep = previous }() + return invoke() + } + results, ok := []invocationOrderResult(nil), false + if memoize { + results, ok = memo.results[key] + } + if !ok { + if memoize { + memo.resolving[key] = true + } + err := ctx.sweepStatementOrderVariantsNested(func(sweep *statementOrderSweep) error { + value, invokeErr := invoke() + if errors.Is(invokeErr, ErrStatementOrderSweepLimit) { + return invokeErr + } + result := invocationOrderResult{ + value: value, + err: invokeErr, + key: invocationOrderResultKey(value, invokeErr), + prefix: append([]int(nil), sweep.taken...), + } + if invokeErr != nil { + result.spelling = "error: " + invokeErr.Error() + } else { + result.spelling = FormatValue(value) + } + if _, exists := memoizedInvocationResult(results, result.key); !exists { + result.first = len(results) == 0 + results = append(results, result) + } + return nil + }) + if memoize { + delete(memo.resolving, key) + } + if err != nil { + return Value{}, err + } + results = orderedInvocationResults(results) + if memoize { + memo.results[key] = results + } + } + if len(results) == 0 { + return Value{}, fmt.Errorf("%w: result of %s under its statement orders was not evaluated", ErrOrderDependentPreview, shape.Label) + } + if len(results) > 1 && ctx.probes > 0 && ctx.statementOrderSweep == nil { + return Value{}, fmt.Errorf("%w: result of %s depends on the statement orders of its body", ErrOrderDependentPreview, shape.Label) + } + selected := 0 + if len(results) > 1 { + alternatives := make([]string, len(results)) + for i := range results { + alternatives[i] = results[i].spelling + } + choice := ChoicePoint{ + Kind: ChoiceStatementOrder, + Step: ctx.enclosingExecutorStep(), + Where: "result of " + shape.Label + " under the statement orders of its body", + Alternatives: alternatives, + } + if ctx.statementOrderSweep != nil { + selected = ctx.statementOrderSweep.choose(&choice) + } else { + choice.Taken = ctx.scheduling().choose(choice, nil) + if err := ctx.scheduling().refusal(); err != nil { + return Value{}, err + } + ctx.noteChoice(choice) + selected = choice.Taken + } + } + result := results[selected] + previous := ctx.statementOrderSweep + ctx.statementOrderSweep = &statementOrderSweep{prefix: append([]int(nil), result.prefix...)} + if memoize { + memo.resolving[key] = true + } + var value Value + var err error + func() { + defer func() { + if memoize { + delete(memo.resolving, key) + } + }() + defer func() { ctx.statementOrderSweep = previous }() + value, err = invoke() + }() + if invocationOrderResultKey(value, err) != result.key { + return Value{}, fmt.Errorf("%w: result of %s did not follow its statement-order witness", ErrOrderDependentPreview, shape.Label) + } + return value, err +} + +func canonicalInvocationArguments(shape *calcShape, args calcArgs) string { + var b strings.Builder + for i, name := range shape.ParamNames { + var value Value + var supplied bool + switch { + case i < len(args.positional): + value, supplied = args.positional[i], true + case args.named != nil: + value, supplied = args.named[name] + } + fmt.Fprintf(&b, "%s=", strconv.Quote(name)) + if !supplied { + b.WriteString("") + } else { + fmt.Fprintf(&b, "%d:%s", value.Kind, strconv.Quote(FormatValue(value))) + } + b.WriteByte(';') + } + if len(args.positional) > len(shape.ParamNames) { + b.WriteString("extra-pos:") + for _, value := range args.positional[len(shape.ParamNames):] { + fmt.Fprintf(&b, "%d:%s;", value.Kind, strconv.Quote(FormatValue(value))) + } + } + names := make([]string, 0, len(args.named)) + for name := range args.named { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + if !shape.hasParameter(name) { + value := args.named[name] + fmt.Fprintf(&b, "unknown:%s=%d:%s;", strconv.Quote(name), value.Kind, strconv.Quote(FormatValue(value))) + continue + } + for i, param := range shape.ParamNames { + if param == name && i < len(args.positional) { + value := args.named[name] + fmt.Fprintf(&b, "duplicate:%s=%d:%s;", strconv.Quote(name), value.Kind, strconv.Quote(FormatValue(value))) + break + } + } + } + return b.String() +} + +func invocationOrderResultKey(value Value, err error) string { + if err != nil { + return "error:" + err.Error() + } + return "value:" + FormatValue(value) +} + +func memoizedInvocationResult(results []invocationOrderResult, key string) (invocationOrderResult, bool) { + for _, result := range results { + if result.key == key { + return result, true + } + } + return invocationOrderResult{}, false +} + +func orderedInvocationResults(results []invocationOrderResult) []invocationOrderResult { + ordered := append([]invocationOrderResult(nil), results...) + sort.SliceStable(ordered, func(i, j int) bool { + a, b := ordered[i], ordered[j] + if (a.err != nil) != (b.err != nil) { + return a.err == nil + } + if a.err == nil && a.first != b.first { + return a.first + } + if a.err != nil { + return a.err.Error() < b.err.Error() + } + return a.spelling < b.spelling + }) + return ordered +} + +func (ctx *Context) invokeOrderedPredicate( + shape *calcShape, + args calcArgs, + self *Instance, + invoke func() (Value, error), +) (Value, error) { + return ctx.invokeWithStatementOrderResults(shape, args, self, true, invoke) +} + +func predicateOrderAware(ctx *Context, sym *symbols.Symbol) bool { + return ctx.scheduling().ordersStatements() && + ctx.reordersTransitively(sym) && ctx.pureTransitively(sym) +} diff --git a/internal/exec/runtime/invoke_action.go b/internal/exec/runtime/invoke_action.go index f122051f69..0fa3ccec39 100644 --- a/internal/exec/runtime/invoke_action.go +++ b/internal/exec/runtime/invoke_action.go @@ -297,9 +297,24 @@ func invokeBoundAction( return nil, nil, fmt.Errorf("invoke action %s: %w", inv.name(), err) } callee.name, callee.out = inv.name(), out + if err := ctx.startShotMove(callee); err != nil { + return nil, nil, err + } return ctx.runCallee(callee) } +// startShotMove pauses a body going one move at a time after a callee's start shot +// read its initial values, a move of its own before its flow's first. +func (ctx *Context) startShotMove(callee *calleeFrame) error { + if !callee.exec.readsAtStart() || callee.exec.state.Ended() { + return nil + } + if err := ctx.tokenStepBody(callee.exec.graph); err != nil { + return ctx.pausing(callee, err) + } + return nil +} + // calleeFrame is an action a body performs as a sub-execution, kept where the body // paused on the action's wait: name is the action as invoked and out the names of // its output parameters, read once it completes. joined marks the performance the diff --git a/internal/exec/runtime/invoke_calc.go b/internal/exec/runtime/invoke_calc.go index ba8433e039..7e0ccc1ef8 100644 --- a/internal/exec/runtime/invoke_calc.go +++ b/internal/exec/runtime/invoke_calc.go @@ -4,6 +4,7 @@ import ( "fmt" "sort" "strings" + "sync" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" @@ -168,7 +169,8 @@ type calcShape struct { BodyOwner *symbols.Symbol // the calc whose body declares Body // Steps is Body without the bindings of its `out` features, which are // evaluated when those features are read rather than run as statements. - Steps []lower.Statement + Steps []lower.Statement + statementOrders sync.Map // Nodes are the action nodes the body's flow performs as the steps of a case. Nodes []ast.Node // BodyOutputs are the output features some statement of the body assigns, @@ -238,7 +240,7 @@ func (ctx *Context) calcInterfaceOf(sym *symbols.Symbol) (*calcShape, error) { return nil, fmt.Errorf("%w: %s states or inherits a result expression from each of %s", ErrConflictingResultExpressions, label, strings.Join(names, ", ")) } - body, bodyOwner := ctx.calcBody(chain) + body, bodyOrder, bodyOwner := ctx.calcBodyWithOrder(chain) shape := &calcShape{ Sym: sym, Name: name, @@ -246,8 +248,12 @@ func (ctx *Context) calcInterfaceOf(sym *symbols.Symbol) (*calcShape, error) { Label: label, Body: body, BodyOwner: bodyOwner, - Steps: calcSteps(body), + Steps: lower.CalcSteps(body), } + if len(shape.Steps) > 0 && bodyOrder != nil { + shape.statementOrders.Store(&shape.Steps[0], bodyOrder) + } + indexCalcStatementOrders(&shape.statementOrders, shape.Steps) shape.Nodes = lower.BlockNodes(shape.Steps) shape.Params = ctx.calcParameters(chain, &shape.Aliases) shape.Outputs = ctx.calcOutputs(chain, &shape.Aliases) @@ -285,6 +291,54 @@ func (ctx *Context) calcInterfaceOf(sym *symbols.Symbol) (*calcShape, error) { return shape, nil } +func indexCalcStatementOrders(orders *sync.Map, stmts []lower.Statement) { + for _, stmt := range stmts { + switch s := stmt.(type) { + case lower.If: + indexCalcBlockOrder(orders, s.Then) + if s.Else != nil { + indexCalcBlockOrder(orders, *s.Else) + } + case lower.Loop: + indexCalcBlockOrder(orders, s.Body) + case lower.Block: + indexCalcBlockOrder(orders, s) + } + } +} + +func indexCalcBlockOrder(orders *sync.Map, block lower.Block) { + if len(block.Statements) > 0 && block.Order != nil { + orders.Store(&block.Statements[0], block.Order) + } + if block.Graph != nil { + indexCalcGraphOrders(orders, block.Graph, make(map[*lower.ActionGraph]bool)) + } + indexCalcStatementOrders(orders, block.Statements) +} + +func indexCalcGraphOrders(orders *sync.Map, graph *lower.ActionGraph, seen map[*lower.ActionGraph]bool) { + if graph == nil || seen[graph] { + return + } + seen[graph] = true + for node, order := range graph.StatementOrders { + stmts := graph.Bodies[node] + if order != nil { + orders.Store(node, order) + } + if len(stmts) > 0 && order != nil { + orders.Store(&stmts[0], order) + } + indexCalcStatementOrders(orders, stmts) + } + for _, subflow := range graph.Subflows { + if subflow != nil { + indexCalcGraphOrders(orders, subflow.Graph, seen) + } + } +} + func calcBindings(chain []*symbols.Symbol) []lower.Binding { var out []lower.Binding for _, link := range chain { @@ -392,21 +446,27 @@ func (ctx *Context) redeclaredIndex(index map[string]int, sym *symbols.Symbol, n // states one, otherwise the closest inherited one — with the calc that declares // it, whose scope the body's statements are written in. func (ctx *Context) calcBody(chain []*symbols.Symbol) ([]lower.Statement, *symbols.Symbol) { + body, _, owner := ctx.calcBodyWithOrder(chain) + return body, owner +} + +func (ctx *Context) calcBodyWithOrder(chain []*symbols.Symbol) ([]lower.Statement, *lower.StatementOrder, *symbols.Symbol) { var stated []lower.Statement + var statedOrder *lower.StatementOrder var owner *symbols.Symbol for i := len(chain) - 1; i >= 0; i-- { link := chain[i] - stmts := lower.CalcBodyWith(link.Decl, unwrappedDeclMembers(link.Decl), link.Scope, ctx.Resolver()) + stmts, order := lower.CalcBodyWithOrder(link.Decl, unwrappedDeclMembers(link.Decl), link.Scope, ctx.Resolver()) if lower.Returns(stmts) { - return stmts, link + return stmts, order, link } // A body that computes but returns nothing leaves an inherited result in // force, so keep looking up the chain before settling for it. if stated == nil && len(stmts) > 0 { - stated, owner = stmts, link + stated, statedOrder, owner = stmts, order, link } } - return stated, owner + return stated, statedOrder, owner } // unboundResultHint explains a `return` that declares a result parameter without @@ -649,6 +709,16 @@ func (ctx *Context) invokeCalcShape(shape *calcShape, args calcArgs, callerScope // invokeCalcShapeIn is invokeCalcShape for a calc declared in a behavior body: // enclosing holds that body's bindings, outermost first, which the calc's own shadow. func (ctx *Context) invokeCalcShapeIn(shape *calcShape, args calcArgs, callerScope *symbols.Scope, self *Instance, enclosing []frame) (Value, error) { + if shape != nil && ctx.scheduling().ordersStatements() && + ctx.reordersTransitively(shape.Sym) && ctx.pureTransitively(shape.Sym) { + return ctx.invokeWithStatementOrderResults(shape, args, self, len(enclosing) == 0, func() (Value, error) { + return ctx.invokeCalcShapeDirect(shape, args, callerScope, self, enclosing) + }) + } + return ctx.invokeCalcShapeDirect(shape, args, callerScope, self, enclosing) +} + +func (ctx *Context) invokeCalcShapeDirect(shape *calcShape, args calcArgs, callerScope *symbols.Scope, self *Instance, enclosing []frame) (Value, error) { if shape.Uncomputed != nil { return Value{}, shape.Uncomputed } @@ -661,7 +731,9 @@ func (ctx *Context) invokeCalcShapeIn(shape *calcShape, args calcArgs, callerSco // a library constant the body reads before the library does, or the body // reads the bindings enclosing it. if ctx.compileCalcs && ctx.trace == nil && len(enclosing) == 0 { - if compiled := ctx.compiledCalcOf(shape); compiled != nil && (self == nil || !compiled.readsLibrary) { + if compiled := ctx.compiledCalcOf(shape); compiled != nil && + (!ctx.scheduling().ordersStatements() || !ctx.reordersTransitively(shape.Sym)) && + (self == nil || !compiled.readsLibrary) { if result, ran, err := compiled.invokeBoxed(ctx, args); ran { return result, err } @@ -1319,7 +1391,7 @@ func (ctx *Context) calcComputes(chain []*symbols.Symbol) bool { return true } var aliases map[string]string - return len(assignedOutputs(calcSteps(body), ctx.calcOutputs(chain, &aliases), aliases)) > 0 + return len(assignedOutputs(lower.CalcSteps(body), ctx.calcOutputs(chain, &aliases), aliases)) > 0 } // isCalcDecl reports whether a declaration is a calc definition or usage, or an diff --git a/internal/exec/runtime/invoke_predicate.go b/internal/exec/runtime/invoke_predicate.go index 8c07087091..3a46941e9f 100644 --- a/internal/exec/runtime/invoke_predicate.go +++ b/internal/exec/runtime/invoke_predicate.go @@ -97,6 +97,17 @@ func (ctx *Context) predicateShapeOf(sym *symbols.Symbol) *calcShape { // invokePredicate applies the predicate sym to args and answers whether its // conditions hold, reading an enclosing run's bindings as a nested calc does. func (ec *EvalContext) invokePredicate(sym *symbols.Symbol, args calcArgs) (Value, error) { + ctx := ec.ctx + shape := ctx.predicateShapeOf(sym) + if predicateOrderAware(ctx, sym) { + return ctx.invokeOrderedPredicate(shape, args, ec.self, func() (Value, error) { + return ec.invokePredicateDirect(sym, args) + }) + } + return ec.invokePredicateDirect(sym, args) +} + +func (ec *EvalContext) invokePredicateDirect(sym *symbols.Symbol, args calcArgs) (Value, error) { ctx := ec.ctx shape := ctx.predicateShapeOf(sym) if err := shape.checkArgs(args); err != nil { diff --git a/internal/exec/runtime/model.go b/internal/exec/runtime/model.go index 97d5bb73de..5b49f22648 100644 --- a/internal/exec/runtime/model.go +++ b/internal/exec/runtime/model.go @@ -3,6 +3,7 @@ package runtime import ( "errors" "sort" + "sync" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" @@ -81,6 +82,11 @@ type Model struct { // defaults, result expression) per calc symbol. calcShapes map[*symbols.Symbol]*calcShape + // constraintSteps memoizes the step each member of a constraint body lowers + // to, one lowering per member node however often a check reads the body. + constraintSteps map[ast.Node]lower.Statement + constraintBodies sync.Map + // predicateShapes memoizes the invocation interfaces of constraints and // requirements applied as predicates. predicateShapes map[*symbols.Symbol]*calcShape @@ -137,6 +143,10 @@ type Model struct { bindingRoots map[*symbols.Symbol]map[string]bool bindingFeatures map[*symbols.Symbol]map[string][]lower.Binding + // namespaceUsageIndex is the model's namespace-owned bindings and subsetting + // usages walked once, on the first namespace denotation that needs them. + namespaceUsageIndex *namespaceModelIndex + // classifierBehaviors memoizes the behaviors each type binds to its objects: // the machines it exhibits and the actions it performs. classifierBehaviors map[*symbols.Symbol][]classifierBehaviorDecl @@ -211,6 +221,7 @@ func NewModel(sem *semantics.Model, resolver *resolve.Resolver) *Model { writeTargets: make(map[writeTargetKey]*writeTarget), calcShapes: make(map[*symbols.Symbol]*calcShape), predicateShapes: make(map[*symbols.Symbol]*calcShape), + constraintSteps: make(map[ast.Node]lower.Statement), librarySymbols: make(map[string]*symbols.Symbol), verificationCases: make(map[*symbols.Scope][]*symbols.Symbol), libraryPerformances: make(map[*symbols.Symbol]*libraryPerformance), @@ -281,6 +292,7 @@ func (m *Model) RegisterScope(scope *symbols.Scope) { m.scopes = append(m.scopes, scope) m.declared = nil m.census = nil + m.namespaceUsageIndex = nil m.behaviorOrdersReady = false m.behaviorOrders = nil m.behaviorOrdersByEnd = nil diff --git a/internal/exec/runtime/namespace_binding.go b/internal/exec/runtime/namespace_binding.go new file mode 100644 index 0000000000..314e0f408b --- /dev/null +++ b/internal/exec/runtime/namespace_binding.go @@ -0,0 +1,774 @@ +package runtime + +import ( + "fmt" + "slices" + "sort" + + "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" +) + +// A namespace-level binding connector (`bind a = b;` owned by a package or +// namespace rather than a type) requires its ends to have the same values +// (KerML 1.0 §7.4.6.3): every usage the run's namespace-owned bindings join — +// a member of one equivalence class — denotes the class's one value. That value +// is the one a member declares or a chain end evaluates to (several, pairwise +// equal or refused), or one object materialized for the earliest-declared +// member and classified by every member's types when none declares one. + +// namespaceModelIndex is the model's namespace-owned bindings and subsetting +// usages walked once over every document's namespace scopes: which usages a +// binding joins (its equivalence class) and which usages subset a namespace +// usage, both in document-name then declaration order. +type namespaceModelIndex struct { + subsetters map[*symbols.Symbol][]*symbols.Symbol + classes map[*symbols.Symbol]*namespaceClass + memberDecls map[ast.Node]*symbols.Symbol +} + +// namespaceClass is one equivalence class of namespace usages joined by +// bindings: its members, the ends carrying a value of their own (a valued +// member or an end that names no usage, evaluated where its binding was +// written), and the bindings for their multiplicity refusal. +type namespaceClass struct { + members []*symbols.Symbol + sources []namespaceSource + bindings []lower.Binding +} + +// namespaceSource is one end carrying a value into its class: a valued usage +// end (usage non-nil) or an end written any other way (usage nil). +type namespaceSource struct { + binding lower.Binding + end int + usage *symbols.Symbol +} + +// namespaceModelIndex builds the index once per Model, on first need. +func (ctx *Context) namespaceModelIndex() *namespaceModelIndex { + if ctx.model.namespaceUsageIndex != nil { + return ctx.model.namespaceUsageIndex + } + out := &namespaceModelIndex{ + subsetters: make(map[*symbols.Symbol][]*symbols.Symbol), + classes: make(map[*symbols.Symbol]*namespaceClass), + memberDecls: make(map[ast.Node]*symbols.Symbol), + } + classOf := func(sym *symbols.Symbol) *namespaceClass { + if class, ok := out.classes[sym]; ok { + return class + } + class := &namespaceClass{} + out.classes[sym] = class + return class + } + merge := func(a, b *symbols.Symbol) *namespaceClass { + ca, cb := classOf(a), classOf(b) + if ca == cb { + return ca + } + ca.members = append(ca.members, cb.members...) + ca.sources = append(ca.sources, cb.sources...) + ca.bindings = append(ca.bindings, cb.bindings...) + for _, member := range cb.members { + out.classes[member] = ca + } + out.classes[b] = ca + return ca + } + var walk func(scope *symbols.Scope) + walk = func(scope *symbols.Scope) { + scope.ForEachMember(func(member *symbols.Symbol) bool { + if member.Scope != nil && member.Scope != scope && namespaceScope(member.Scope) { + walk(member.Scope) + return true + } + member = ctx.declaredSymbol(member) + if _, ok := member.Decl.(*ast.Usage); !ok { + return true + } + for _, rel := range relationshipsOfKind(member, ast.RelSubsets) { + if target := ctx.model.semantics.RelationshipTarget(member, rel); target != nil { + out.subsetters[target] = append(out.subsetters[target], member) + } + } + return true + }) + for _, binding := range lower.NamespaceBindings(scope) { + var class *namespaceClass + for end := range binding.Ends { + if u, ok := ctx.namespaceEndUsage(binding, end); ok { + if class == nil { + class = classOf(u) + } else { + class = merge(class.members[len(class.members)-1], u) + } + if !containsSymbol(class.members, u) { + class.members = append(class.members, u) + } + } + } + if class == nil { + continue + } + class.bindings = append(class.bindings, binding) + for end := range binding.Ends { + u, named := ctx.namespaceEndUsage(binding, end) + if named { + if decl, isUsage := u.Decl.(*ast.Usage); isUsage && decl.Value != nil { + class.sources = append(class.sources, namespaceSource{binding: binding, end: end, usage: u}) + } + } else { + class.sources = append(class.sources, namespaceSource{binding: binding, end: end}) + } + } + } + } + if ctx.model.resolver != nil { + if idx := ctx.model.resolver.Index(); idx != nil { + for _, doc := range idx.Documents() { + walk(idx.DocumentRoot(doc)) + } + } + } + for _, class := range out.classes { + sort.SliceStable(class.members, func(i, j int) bool { return declaredBefore(class.members[i], class.members[j]) }) + for _, member := range class.members { + if member.Decl != nil { + out.memberDecls[member.Decl] = member + } + } + } + for _, subs := range out.subsetters { + sort.SliceStable(subs, func(i, j int) bool { return declaredBefore(subs[i], subs[j]) }) + } + ctx.model.namespaceUsageIndex = out + return out +} + +// containsSymbol reports whether syms holds sym. +func containsSymbol(syms []*symbols.Symbol, sym *symbols.Symbol) bool { + for _, s := range syms { + if s == sym { + return true + } + } + return false +} + +// namespaceEndUsage resolves a binding end written as a name to the usage it +// declares; an end written any other way names no usage at namespace level. +func (ctx *Context) namespaceEndUsage(binding lower.Binding, end int) (*symbols.Symbol, bool) { + expr := binding.Ends[end].Expr + if expr == nil { + return nil, false + } + qn := ast.AsQualifiedName(expr) + if qn == nil { + return nil, false + } + sym, ok := ctx.resolveQualified(binding.Scope, qn) + if !ok || sym == nil { + return nil, false + } + if _, ok := sym.Decl.(*ast.Usage); !ok { + return nil, false + } + return sym, true +} + +// namespaceBoundObjects is the objects a namespace usage joined by a namespace-owned +// binding denotes. bound reports whether a binding governs the usage at all. +func (ctx *Context) namespaceBoundObjects(sym *symbols.Symbol) (objs []*Instance, bound bool, err error) { + if val, ok := ctx.namespaceBindings[sym]; ok { + return ctx.liveInstances(boundPrefixObjects(val)), true, nil + } + class, member := ctx.namespaceClassMember(sym) + if class == nil { + if live, ok := ctx.liveOccurrences(sym); ok { + return live, true, nil + } + return nil, false, nil + } + return ctx.resolveNamespaceClass(class, member) +} + +// namespaceClassMember is the binding class sym belongs to and its member +// declaring sym: a reader may hold a different scope tree's symbol for one +// declaration than the index's tree built the class of, so a missed symbol +// match is retried on the declaration. +func (ctx *Context) namespaceClassMember(sym *symbols.Symbol) (*namespaceClass, *symbols.Symbol) { + nmi := ctx.namespaceModelIndex() + if class := nmi.classes[sym]; class != nil { + return class, sym + } + member := nmi.memberDecls[sym.Decl] + if member == nil { + return nil, nil + } + return nmi.classes[member], member +} + +// liveInstances is the live objects ids names, in order. +func (ctx *Context) liveInstances(ids []int64) []*Instance { + out := make([]*Instance, 0, len(ids)) + for _, id := range ids { + if inst, live := ctx.instances[id]; live { + out = append(out, inst) + } + } + return out +} + +// resolveNamespaceClass makes every member of a binding's equivalence class denote +// the class's one value, reporting the objects the member asking for (want) +// denotes. A second call for the class under way answers not-bound so a member +// being evaluated resolves as usual — its declared value cycles through the +// binding stack — while a chain end reading a member of its own class is the +// cyclic dependency CyclicBindingError reports. +func (ctx *Context) resolveNamespaceClass(class *namespaceClass, want *symbols.Symbol) ([]*Instance, bool, error) { + if ctx.resolvingNamespaceClasses[class] { + if ctx.namespaceChainReads[class] { + return nil, true, &CyclicBindingError{Usage: want, Stated: ctx.qualifiedSymbolName(want)} + } + return nil, false, nil + } + for _, binding := range class.bindings { + if err := ctx.namespaceBindingCounts(binding); err != nil { + return nil, true, err + } + } + ctx.resolvingNamespaceClasses[class] = true + defer delete(ctx.resolvingNamespaceClasses, class) + + type held struct { + val Value + text string + } + var vals []held + seen := make(map[*symbols.Symbol]bool) + recorded := func() { + for _, member := range class.members { + if seen[member] { + continue + } + if val, ok := ctx.namespaceBindings[member]; ok { + vals = append(vals, held{val, symbolText(member)}) + seen[member] = true + continue + } + if live, ok := ctx.liveOccurrences(member); ok { + elements := make([]Value, 0, len(live)) + for _, inst := range live { + val, err := ctx.objectValue(inst) + if err != nil { + return + } + elements = append(elements, val) + } + val := sequenceOf(elements) + if len(elements) == 1 { + val = elements[0] + } + vals = append(vals, held{val, symbolText(member)}) + seen[member] = true + } + } + } + recorded() + for _, src := range class.sources { + if src.usage != nil { + if seen[src.usage] { + continue + } + seen[src.usage] = true + decl := src.usage.Decl.(*ast.Usage) + val, err := NewEvalContext(ctx, src.usage.OwnerScope).declaredValue(src.usage, decl.Value) + if err != nil { + return nil, true, err + } + vals = append(vals, held{val, ctx.bindingEndpointText(src.binding, src.end)}) + continue + } + ctx.namespaceChainReads[class] = true + val, err := NewEvalContext(ctx, src.binding.Scope).Eval(src.binding.Ends[src.end].Expr) + delete(ctx.namespaceChainReads, class) + if err != nil { + return nil, true, err + } + vals = append(vals, held{val, ctx.bindingEndpointText(src.binding, src.end)}) + } + // Members a source's evaluation denoted contribute their value to the class. + recorded() + for i := range vals { + for j := i + 1; j < len(vals); j++ { + if !ctx.equalValues(vals[i].val, vals[j].val) { + return nil, true, &BindingConflictError{ + Left: vals[i].text, + Right: vals[j].text, + LeftValue: vals[i].val, + RightValue: vals[j].val, + } + } + } + } + if len(vals) == 0 { + // A class whose members denote no objects has nothing to materialize: + // a valueless scalar binding leaves each member undetermined. + objectBearing := false + for _, member := range class.members { + if ctx.namesOneObject(member) || ctx.namesObjects(member) || + isOccurrenceUsage(member) || objectFeature(member) { + objectBearing = true + break + } + } + if !objectBearing { + return nil, false, nil + } + // The class's one value is what the members' own denotations would + // materialize: the largest lower bound among them, made the way + // occurrencesOf makes them, so which member is read first does not + // change it. + earliest := class.members[0] + var count int64 + for _, member := range class.members { + n, err := ctx.lowerBoundCount(ctx.featureMultiplicity(member, ctx.findOwnerType(member)), 0, symbolText(member)) + if err != nil { + return nil, true, fmt.Errorf("usage %s: %w", symbolText(member), err) + } + if n > count { + count = n + } + } + // A class of collections bigger than eagerLowerBound shares one required + // population rather than materializing its count, as occurrencesOf does — + // only while every member names objects of the earliest's type held + // lazily, so no member classifies the shared objects another way. + lazy := true + for _, member := range class.members { + if !ctx.namesObjects(member) || !ctx.holdsLazily(member, count) { + lazy = false + break + } + if member != earliest && !ctx.classifiesAlike(member, earliest) { + lazy = false + break + } + } + if lazy { + seq, err := ctx.withRequired(nil, earliest, nil, "", count) + if err != nil { + return nil, true, err + } + r := seq.required + val := NewSequenceValue(seq) + text := symbolText(earliest) + if len(class.bindings) > 0 { + text = ctx.bindingText(class.bindings[0]) + } + fail := func(err error) ([]*Instance, bool, error) { + for _, member := range class.members { + delete(ctx.occurrences, member) + ctx.unbindNamespace(member) + delete(ctx.occurrenceTails, member) + } + ctx.required = slices.DeleteFunc(ctx.required, func(e *requiredMembers) bool { return e == r }) + return nil, true, err + } + for _, member := range class.members { + if msg := ctx.featureMultiplicity(member, ctx.findOwnerType(member)).CountViolation(int64(seq.Size())); msg != "" { + return fail(fmt.Errorf("%w: `%s`: %s", ErrBindingConflict, text, msg)) + } + ctx.occurrences[member] = []int64{} + ctx.bindNamespace(member, val) + ctx.recordOccurrenceTail(member, r) + } + if val, ok := ctx.namespaceBindings[want]; ok { + return ctx.liveInstances(boundPrefixObjects(val)), true, nil + } + if live, ok := ctx.liveOccurrences(want); ok { + return live, true, nil + } + return nil, true, nil + } + release := ctx.elementScope() + if err := ctx.chargeElements(count); err != nil { + release() + return nil, true, err + } + mark := len(ctx.created) + members, err := ctx.materializeMembers(earliest, int(count), nil, "") + if err != nil { + ctx.abandonInstancesSince(mark) + release() + return nil, true, fmt.Errorf("usage %s: %w", symbolText(earliest), err) + } + elements := make([]Value, 0, len(members)) + for _, inst := range members { + obj, err := ctx.objectValue(inst) + if err != nil { + ctx.abandonInstancesSince(mark) + release() + return nil, true, err + } + elements = append(elements, obj) + } + val := sequenceOf(elements) + if len(elements) == 1 { + val = elements[0] + } + text := symbolText(earliest) + if len(class.bindings) > 0 { + text = ctx.bindingText(class.bindings[0]) + } + // Recorded before the conform checks, which can start classifier + // behaviors of the shared objects: one of them reading a member reaches + // the objects the class already names rather than materializing another. + for _, member := range class.members { + ctx.occurrences[member] = boundPrefixObjects(val) + ctx.bindNamespace(member, val) + } + fail := func(err error) ([]*Instance, bool, error) { + for _, member := range class.members { + delete(ctx.occurrences, member) + ctx.unbindNamespace(member) + } + ctx.abandonInstancesSince(mark) + release() + return nil, true, err + } + count64 := int64(len(members)) + for _, member := range class.members { + if msg := ctx.featureMultiplicity(member, ctx.findOwnerType(member)).CountViolation(count64); msg != "" { + return fail(fmt.Errorf("%w: `%s`: %s", ErrBindingConflict, text, msg)) + } + // A member typed another way classifies the shared objects first, + // so its declared-value check admits them. + if member != earliest { + if err := ctx.classifyHeld(member, val); err != nil { + return fail(err) + } + } + v, err := NewEvalContext(ctx, member.OwnerScope).conformDeclared(member, val) + if err != nil { + return fail(fmt.Errorf("%w: `%s`: %v", ErrBindingConflict, text, err)) + } + ctx.occurrences[member] = boundPrefixObjects(v) + ctx.bindNamespace(member, v) + } + if err := ctx.startClassifierBehaviorsOf(members, mark); err != nil { + return fail(err) + } + release() + } else { + val := vals[0].val + text := symbolText(class.members[0]) + if len(class.bindings) > 0 { + text = ctx.bindingText(class.bindings[0]) + } + // A value the class took can end in required members its source holds + // lazily: each member shares them through the tail recorded for it, so + // its occurrences see every object the value denotes. The tail belongs + // to the source's population, so a failure only undoes these records. + var recorded []*symbols.Symbol + fail := func(err error) ([]*Instance, bool, error) { + for _, member := range recorded { + delete(ctx.occurrenceTails, member) + } + return nil, true, err + } + for _, member := range class.members { + if seen[member] { + if seq := requiredTail(val); seq != nil && ctx.occurrenceTails[member] == nil { + ctx.recordOccurrenceTail(member, seq.required) + recorded = append(recorded, member) + } + continue + } + ec := NewEvalContext(ctx, member.OwnerScope) + v, err := ec.conformDeclared(member, val) + if err != nil { + return fail(fmt.Errorf("%w: `%s`: %v", ErrBindingConflict, text, err)) + } + ctx.occurrences[member] = boundPrefixObjects(v) + if seq := requiredTail(v); seq != nil { + ctx.recordOccurrenceTail(member, seq.required) + recorded = append(recorded, member) + } + ctx.bindNamespace(member, v) + } + } + if val, ok := ctx.namespaceBindings[want]; ok { + return ctx.liveInstances(boundPrefixObjects(val)), true, nil + } + if live, ok := ctx.liveOccurrences(want); ok { + return live, true, nil + } + return nil, true, nil +} + +// classifiesAlike reports whether member classifies the class's shared objects as +// earliest does: the same type, no members of its own to confer, and no +// relationship but typing — anything else the eager path classifies objects by. +func (ctx *Context) classifiesAlike(member, earliest *symbols.Symbol) bool { + if ctx.extractType(member) != ctx.extractType(earliest) { + return false + } + if member.Scope != nil && len(member.Scope.Members()) > 0 { + return false + } + for _, rel := range semantics.RelationshipsOf(member) { + if rel == nil || rel.Kind == ast.RelTyping { + continue + } + return false + } + return true +} + +// boundPrefixObjects is the made objects a binding's value denotes as the positions +// before any required members it ends in: a made required member is reached at its +// reserved position through the tail, not as the next denoted object. +func boundPrefixObjects(val Value) []int64 { + if seq := requiredTail(val); seq != nil { + var out []int64 + for _, element := range seq.elements { + if id, ok := element.Object(); ok { + out = append(out, id) + } + } + return out + } + return heldObjects(val) +} + +// namespaceBoundValue is the value a usage a binding connector governs reads as: +// the value resolving its class recorded for it when one was kept, else the live +// objects it denotes — as a read through the binding answers. +func (ctx *Context) namespaceBoundValue(sym *symbols.Symbol) (Value, bool, error) { + objs, bound, err := ctx.namespaceBoundObjects(sym) + if err != nil || !bound { + return Value{}, bound, err + } + if val, ok := ctx.namespaceBindings[sym]; ok { + return val, true, nil + } + if _, member := ctx.namespaceClassMember(sym); member != sym { + if val, ok := ctx.namespaceBindings[member]; ok { + return val, true, nil + } + } + elements := make([]Value, 0, len(objs)) + for _, inst := range objs { + obj, err := ctx.objectValue(inst) + if err != nil { + return Value{}, true, err + } + elements = append(elements, obj) + } + val := sequenceOf(elements) + if len(elements) == 1 { + val = elements[0] + } + return val, true, nil +} + +// namespaceBindingCounts refuses an end or connector multiplicity other than the one +// link a binding connector declares, as a binding between object features is refused. +func (ctx *Context) namespaceBindingCounts(binding lower.Binding) error { + for end := range binding.Ends { + if r, ok := ctx.model.semantics.RangeIn(binding.Scope, binding.Ends[end].Multiplicity); ok { + if n, exact := r.Exactly(); !exact || n != 1 { + return &UndeterminedBindingError{ + Target: ctx.bindingText(binding), + Binding: ctx.bindingText(binding), + Endpoint: ctx.bindingEndpointText(binding, end), + Other: ctx.bindingEndpointText(binding, 1-end), + } + } + } + } + if r, ok := ctx.model.semantics.RangeIn(binding.Scope, binding.Multiplicity); ok { + if n, exact := r.Exactly(); !exact || n != 1 { + return fmt.Errorf("%w: `%s` declares %s link(s) but joins namespace usages, each of which denotes one value", + ErrBindingEnd, ctx.bindingText(binding), r.Text()) + } + } + return nil +} + +// namespacedSubsetObjects is the objects a namespace usage takes from the usages +// subsetting it, as a composite collection takes its subsetting features' values +// (KerML 1.0 §7.3.4.4): their objects are members, optional subsetters with room and +// then anonymous objects make up the lower bound, and an abstract usage holds its +// concrete specializations' values alone. ok reports whether any usage subsets sym. +func (ctx *Context) namespacedSubsetObjects(sym *symbols.Symbol) ([]*Instance, bool, error) { + if sym == nil || sym.OwnerScope == nil || !namespaceScope(sym.OwnerScope) { + return nil, false, nil + } + decl, ok := sym.Decl.(*ast.Usage) + if !ok || decl.Value != nil || ctx.model.semantics.IsVariationFeature(sym) { + return nil, false, nil + } + if !isOccurrenceUsage(sym) && !objectFeature(sym) { + return nil, false, nil + } + subs := ctx.namespaceModelIndex().subsetters[sym] + if len(subs) == 0 { + return nil, false, nil + } + if live, ok := ctx.liveOccurrences(sym); ok { + return live, true, nil + } + if ctx.namespaceCollecting[sym] { + return nil, true, fmt.Errorf("%w: usage %s subsets itself", ErrCyclicFeatureValue, symbolText(sym)) + } + ctx.namespaceCollecting[sym] = true + defer delete(ctx.namespaceCollecting, sym) + + release := ctx.elementScope() + var ids []int64 + var tail *requiredMembers + fail := func(err error) ([]*Instance, bool, error) { + if tail != nil { + delete(ctx.occurrenceTails, sym) + ctx.required = slices.DeleteFunc(ctx.required, func(e *requiredMembers) bool { return e == tail }) + } + release() + return nil, true, err + } + + var contributed []*Instance + var optional []*symbols.Symbol + for _, sub := range subs { + if ctx.optionalValueless(sub) { + optional = append(optional, sub) + continue + } + objs, err := ctx.denotedSubsetObjects(sub) + if err != nil { + return fail(fmt.Errorf("subsetting usage %s of %s: %w", symbolText(sub), symbolText(sym), err)) + } + for _, inst := range objs { + if !containsInstance(contributed, inst.ID) { + contributed = append(contributed, inst) + ids = append(ids, inst.ID) + } + } + } + + mult := ctx.featureMultiplicity(sym, ctx.findOwnerType(sym)) + abstract := symbols.IsAbstract(sym) + if !abstract { + count, err := ctx.lowerBoundCount(mult, len(contributed), fmt.Sprintf("usage %s", symbolText(sym))) + if err != nil { + return fail(err) + } + // The collection's own fill is charged and made eagerly, or held as + // required members past eagerLowerBound, as occurrencesOf holds them. + lazy := ctx.holdsLazily(sym, count) + if !lazy { + if err := ctx.chargeElements(count); err != nil { + return fail(err) + } + } + mark := len(ctx.created) + var newObjs []*Instance + for _, sub := range optional { + if count == 0 { + break + } + upper := ctx.featureMultiplicity(sub, ctx.findOwnerType(sub)).Upper + spare := count + if upper.Known && !upper.Infinite && upper.Value < spare { + spare = upper.Value + } + // A lazily held fill is uncharged until now, so its optional subsetters + // charge what they are about to make — an open upper bound refuses whole. + if lazy { + if err := ctx.chargeElements(spare); err != nil { + ctx.abandonInstancesSince(mark) + return fail(err) + } + } + var filled []int64 + for i := int64(0); i < spare; i++ { + inst, err := ctx.materialize(sub, 0, nil, "") + if err != nil { + ctx.abandonInstancesSince(mark) + return fail(err) + } + filled = append(filled, inst.ID) + newObjs = append(newObjs, inst) + count-- + } + if len(filled) > 0 { + ctx.occurrences[sub] = filled + } + } + if lazy { + if count > 0 { + seq, err := ctx.withRequired(nil, sym, nil, "", count) + if err != nil { + ctx.abandonInstancesSince(mark) + return fail(err) + } + tail = seq.required + ctx.recordOccurrenceTail(sym, tail) + count = 0 + } + } + made, err := ctx.materializeMembers(sym, int(count), nil, "") + if err != nil { + ctx.abandonInstancesSince(mark) + return fail(err) + } + newObjs = append(newObjs, made...) + for _, inst := range newObjs { + contributed = append(contributed, inst) + ids = append(ids, inst.ID) + } + if err := ctx.startClassifierBehaviorsOf(newObjs, mark); err != nil { + ctx.abandonInstancesSince(mark) + return fail(err) + } + } + var tailCount int64 + if tail != nil { + tailCount = tail.count + } + if msg := mult.CountViolation(int64(len(contributed)) + tailCount); msg != "" { + return fail(fmt.Errorf("usage %s: %w: %s", symbolText(sym), ErrMultiplicityViolation, msg)) + } + ctx.occurrences[sym] = ids + release() + return contributed, true, nil +} + +// denotedSubsetObjects is the objects a usage subsetting a namespace usage +// contributes: what it denotes, or what its declared value binds it to. +func (ctx *Context) denotedSubsetObjects(sym *symbols.Symbol) ([]*Instance, error) { + if namespaceObjectUsage(sym) { + ec := NewEvalContext(ctx, sym.OwnerScope) + val, err := ec.declaredValue(sym, sym.Decl.(*ast.Usage).Value) + if err != nil { + return nil, err + } + return ctx.liveInstances(heldObjects(val)), nil + } + return ctx.denotedObjects(sym) +} + +// containsInstance reports whether objs holds the object id names. +func containsInstance(objs []*Instance, id int64) bool { + for _, inst := range objs { + if inst.ID == id { + return true + } + } + return false +} diff --git a/internal/exec/runtime/order_analysis.go b/internal/exec/runtime/order_analysis.go new file mode 100644 index 0000000000..d05567f433 --- /dev/null +++ b/internal/exec/runtime/order_analysis.go @@ -0,0 +1,271 @@ +package runtime + +import ( + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast/astcodec" +) + +type bodyOrderAnalysis struct { + ownReorders bool + ownEffects bool + reorders bool + effects bool + calls []*bodyOrderAnalysis +} + +func (ctx *Context) reordersTransitively(sym *symbols.Symbol) bool { + analysis := ctx.analyzeBodyOrder(sym) + return analysis != nil && analysis.reorders +} + +func (ctx *Context) pureTransitively(sym *symbols.Symbol) bool { + analysis := ctx.analyzeBodyOrder(sym) + return analysis != nil && !analysis.effects +} + +func (ctx *Context) analyzeBodyOrder(sym *symbols.Symbol) *bodyOrderAnalysis { + analysis := ctx.analyzeBodyOrderNode(sym) + ctx.settleBodyOrderAnalysis() + return analysis +} + +func (ctx *Context) analyzeBodyOrderNode(sym *symbols.Symbol) *bodyOrderAnalysis { + if sym == nil { + return nil + } + if ctx.orderAnalysis == nil { + ctx.orderAnalysis = make(map[*symbols.Symbol]*bodyOrderAnalysis) + } + if analysis := ctx.orderAnalysis[sym]; analysis != nil { + return analysis + } + analysis := &bodyOrderAnalysis{} + ctx.orderAnalysis[sym] = analysis + switch { + case isCalcSymbol(sym): + shape, err := ctx.calcShapeOf(sym) + if err == nil { + shape.statementOrders.Range(func(_, value any) bool { + if order, ok := value.(*lower.StatementOrder); ok && order.Reorders(false) { + analysis.reorders = true + return false + } + return true + }) + ctx.analyzeOrderStatements(shape.Steps, analysis) + if shape.performs() { + analysis.effects = true + } + for _, param := range shape.Params { + ctx.analyzeOrderExpression(param.Default, ctx.calcScope(param.Owner, shape.Sym, nil), analysis) + } + for _, binding := range shape.Bindings { + for _, end := range binding.Ends { + ctx.analyzeOrderExpression(end.Expr, binding.Scope, analysis) + } + } + ctx.analyzeOrderExpression(shape.ResultExpr, shape.bodyScope(), analysis) + } + case isPredicateDecl(sym.Decl): + conditions := ctx.ConditionsOf(sym, sym.Scope) + ctx.analyzeOrderConditions(conditions, analysis) + for _, feature := range ctx.conditionFeatures(sym) { + ctx.analyzeOrderExpression(feature.expr, feature.scope, analysis) + } + } + analysis.ownReorders, analysis.ownEffects = analysis.reorders, analysis.effects + return analysis +} + +func (ctx *Context) settleBodyOrderAnalysis() { + for changed := true; changed; { + changed = false + for _, analysis := range ctx.orderAnalysis { + reorders, effects := analysis.ownReorders, analysis.ownEffects + for _, callee := range analysis.calls { + reorders = reorders || callee.reorders + effects = effects || callee.effects + } + if analysis.reorders != reorders || analysis.effects != effects { + analysis.reorders, analysis.effects = reorders, effects + changed = true + } + } + } +} + +func (ctx *Context) analyzeOrderConditions(conditions []Condition, analysis *bodyOrderAnalysis) { + for _, condition := range conditions { + if condition.Steps != nil { + if condition.Steps.Order != nil && condition.Steps.Order.Reorders(false) { + analysis.reorders = true + } + for _, write := range condition.Steps.Footprint.Writes { + if !write.Local { + analysis.effects = true + } + } + ctx.analyzeOrderStatements(condition.Steps.Stmts, analysis) + } + ctx.analyzeOrderExpression(condition.Expr, condition.Scope, analysis) + for _, constraint := range condition.Constraints { + ctx.analyzeOrderCallee(constraint, analysis) + } + ctx.analyzeOrderConditions(condition.Group, analysis) + } +} + +func (ctx *Context) analyzeOrderStatements(stmts []lower.Statement, analysis *bodyOrderAnalysis) { + for _, stmt := range stmts { + switch statement := stmt.(type) { + case lower.Send: + analysis.effects = true + ctx.analyzeOrderExpression(statement.Message, statement.Scope, analysis) + ctx.analyzeOrderExpression(statement.TargetExpr, statement.Scope, analysis) + ctx.analyzeOrderExpression(statement.ReceiverExpr, statement.Scope, analysis) + case lower.Assign: + ctx.analyzeOrderExpression(statement.Value, statement.Scope, analysis) + if statement.Chain != nil { + analysis.effects = true + ctx.analyzeOrderExpression(statement.Chain.Base, statement.Scope, analysis) + } + case lower.Declare: + ctx.analyzeOrderExpression(statement.Value, statement.Scope, analysis) + case lower.DeclareUsage: + case lower.Block: + if statement.Order != nil && statement.Order.Reorders(false) { + analysis.reorders = true + } + ctx.analyzeOrderStatements(statement.Statements, analysis) + ctx.analyzeOrderGraph(statement.Graph, analysis) + case lower.Loop: + ctx.analyzeOrderExpression(statement.Condition, statement.Scope, analysis) + ctx.analyzeOrderExpression(statement.Until, statement.Scope, analysis) + ctx.analyzeOrderExpression(statement.Collection, statement.Scope, analysis) + ctx.analyzeOrderStatements(statement.Body.Statements, analysis) + ctx.analyzeOrderGraph(statement.Body.Graph, analysis) + if statement.Body.Order != nil && statement.Body.Order.Reorders(false) { + analysis.reorders = true + } + case lower.If: + ctx.analyzeOrderExpression(statement.Condition, statement.Scope, analysis) + ctx.analyzeOrderStatements(statement.Then.Statements, analysis) + ctx.analyzeOrderGraph(statement.Then.Graph, analysis) + if statement.Then.Order != nil && statement.Then.Order.Reorders(false) { + analysis.reorders = true + } + if statement.Else != nil { + ctx.analyzeOrderStatements(statement.Else.Statements, analysis) + ctx.analyzeOrderGraph(statement.Else.Graph, analysis) + if statement.Else.Order != nil && statement.Else.Order.Reorders(false) { + analysis.reorders = true + } + } + case lower.Return: + ctx.analyzeOrderExpression(statement.Value, statement.Scope, analysis) + case lower.Effect: + analysis.effects = true + ctx.analyzeOrderExpression(statement.TargetExpr, statement.Scope, analysis) + case lower.Assert: + ctx.analyzeOrderCallee(statement.Sym, analysis) + case lower.Unsupported: + } + } +} + +func (ctx *Context) analyzeOrderGraph(graph *lower.ActionGraph, analysis *bodyOrderAnalysis) { + if graph == nil { + return + } + if len(graph.Connections) > 0 || len(graph.DataFlows) > 0 || len(graph.Accepts) > 0 { + analysis.effects = true + } + for _, node := range graph.Nodes { + if usage, ok := node.(*ast.Usage); ok { + if _, performs := nestedInvocation(usage); performs || connectsPins(graph, usage) { + analysis.effects = true + } + } + } + for _, order := range graph.StatementOrders { + if order != nil && order.Reorders(false) { + analysis.reorders = true + } + } + for _, stmts := range graph.Bodies { + ctx.analyzeOrderStatements(stmts, analysis) + } + for _, attribute := range graph.Attributes { + ctx.analyzeOrderExpression(attribute.Value, attribute.Scope, analysis) + } + for _, features := range graph.Features { + for _, feature := range features { + ctx.analyzeOrderExpression(feature.Value, feature.Scope, analysis) + } + } + for _, binding := range append(append([]lower.PinBinding(nil), graph.Bindings...), graph.ValueBindings...) { + ctx.analyzeOrderExpression(binding.Other, binding.Scope, analysis) + if binding.OtherChain != nil { + ctx.analyzeOrderExpression(binding.OtherChain.Base, binding.Scope, analysis) + } + } + for _, accept := range graph.Accepts { + ctx.analyzeOrderExpression(accept.Trigger, accept.Scope, analysis) + } + for _, subflow := range graph.Subflows { + if subflow != nil { + ctx.analyzeOrderGraph(subflow.Graph, analysis) + } + } +} + +func (ctx *Context) analyzeOrderExpression(expression ast.Node, scope *symbols.Scope, analysis *bodyOrderAnalysis) { + if expression == nil || ctx.model == nil || ctx.model.resolver == nil { + return + } + for node := range astcodec.Reachable(expression) { + var candidates []*symbols.Symbol + switch expression := node.(type) { + case *ast.InvocationExpr: + if expression.Type != nil { + candidates = ctx.model.resolver.InvocationCandidates(scope, expression.Type) + } + case *ast.FeatureReference: + if expression.Name != nil { + if sym, ok := ctx.model.resolver.ResolveQualified(scope, expression.Name); ok { + candidates = []*symbols.Symbol{sym} + } + } + } + for _, candidate := range candidates { + ctx.analyzeOrderCallee(candidate, analysis) + } + } +} + +func (ctx *Context) analyzeOrderCallee(sym *symbols.Symbol, analysis *bodyOrderAnalysis) { + if sym == nil { + return + } + if sym.Kind == symbols.SymbolAlias && ctx.model != nil && ctx.model.resolver != nil { + if target, ok := ctx.model.resolver.ResolveAliasTarget(sym); ok { + ctx.analyzeOrderCallee(target, analysis) + } + return + } + ctx.mergeOrderAnalysis(ctx.analyzeBodyOrderNode(sym), analysis) +} + +func (ctx *Context) mergeOrderAnalysis(from, into *bodyOrderAnalysis) { + if from == nil || into == nil { + return + } + for _, callee := range into.calls { + if callee == from { + return + } + } + into.calls = append(into.calls, from) +} diff --git a/internal/exec/runtime/replay.go b/internal/exec/runtime/replay.go index 6b79713b64..ac0d8673bc 100644 --- a/internal/exec/runtime/replay.go +++ b/internal/exec/runtime/replay.go @@ -590,7 +590,7 @@ func readHeader(text string) (w Witness, headed bool, err error) { // ParseChoice reads one choice as ChoiceTaken.String spells it: `step N: T first of A, B`, // `step N: decision D -> B`, `S -> T`, `W: X first of A, B` (a region order, a due -// order at `t=…`, or a dispatch order among `events at t=…`). +// order at `t=…`, or a dispatch order among `events at t=…`), `step N: statements in …: X first of A, B`. func ParseChoice(text string) (ChoiceTaken, error) { text = strings.TrimSpace(text) fail := func(reason string) (ChoiceTaken, error) { @@ -623,8 +623,9 @@ func ParseChoice(text string) (ChoiceTaken, error) { func parseOrderChoice(fail func(string) (ChoiceTaken, error), step int, first, mark, after string) (ChoiceTaken, error) { c := ChoiceTaken{Kind: ChoiceTokenOrder, Step: step, Took: first} if mark == markWhere { - if step > 0 { - return fail("a step's order names the token first: step : first of …") + if step > 0 && !strings.HasPrefix(first, statementsWherePrefix) && + !strings.HasPrefix(first, guardOrderWherePrefix) && !strings.HasPrefix(first, resultOrderWherePrefix) { + return fail("a step's order names the token, guard, or result first: step : : first of …") } c.Kind, c.Where = ChoiceRegionOrder, first switch { @@ -638,6 +639,12 @@ func parseOrderChoice(fail func(string) (ChoiceTaken, error), step int, first, m c.Kind = ChoiceExitOrder case strings.HasPrefix(first, entryStepWherePrefix): c.Kind = ChoiceEntryStep + case strings.HasPrefix(first, statementsWherePrefix): + c.Kind = ChoiceStatementOrder + case strings.HasPrefix(first, guardOrderWherePrefix): + c.Kind = ChoiceGuardOrder + case strings.HasPrefix(first, resultOrderWherePrefix): + c.Kind = ChoiceStatementOrder case strings.HasPrefix(first, stepWherePrefix): c.Kind = ChoiceStepOrder } diff --git a/internal/exec/runtime/replay_test.go b/internal/exec/runtime/replay_test.go index 8d3f9101f3..de234c1b65 100644 --- a/internal/exec/runtime/replay_test.go +++ b/internal/exec/runtime/replay_test.go @@ -23,6 +23,8 @@ func TestParseChoiceReadsEveryKind(t *testing.T) { }{ {"step 3: 2@left first of 2@left, 3@right", ChoiceTaken{Kind: ChoiceTokenOrder, Step: 3, Alternatives: 2, Taken: 0, Among: []string{"2@left", "3@right"}, Took: "2@left"}}, {"step 3: 3@right first of 2@left, 3@right", ChoiceTaken{Kind: ChoiceTokenOrder, Step: 3, Alternatives: 2, Taken: 1, Among: []string{"2@left", "3@right"}, Took: "3@right"}}, + {"step 1: result of calc test::Ord under the statement orders of its body: 30 first of 12, 30", + ChoiceTaken{Kind: ChoiceStatementOrder, Step: 1, Where: "result of calc test::Ord under the statement orders of its body", Alternatives: 2, Taken: 1, Among: []string{"12", "30"}, Took: "30"}}, {"step 5: decision select -> 2->alarm", ChoiceTaken{Kind: ChoiceDecisionBranch, Step: 5, Where: "decision select", Took: "2->alarm"}}, {"state idle on accept go -> 2->right", ChoiceTaken{Kind: ChoiceTransition, Where: "state idle on accept go", Took: "2->right"}}, {"on accept go: b1 first of a1, b1", ChoiceTaken{Kind: ChoiceRegionOrder, Where: "on accept go", Alternatives: 2, Taken: 1, Among: []string{"a1", "b1"}, Took: "b1"}}, @@ -485,7 +487,7 @@ func dueOrderModel(t *testing.T) (func() (*Context, error), func(*Context) (Outc state waiting; accept after 5 [s] then took; state took { - entry action take { assign seen := cell.mark; assign cell.mark := cell.mark + 1; } + entry action take { assign seen := cell.mark; then assign cell.mark := cell.mark + 1; } } } state a : Ticker; @@ -655,11 +657,14 @@ func TestReplayRefusesAMoveNotEnabled(t *testing.T) { } x := m.exploreAction(t, "explore", "route") good := x.Outcomes[0].Witness - const wantGood = "step 3: 2@a first of 2@a, 3@b, 4@c; step 4: 3@b first of 3@b, 4@c; step 7: decision select -> 1->warn" + const wantGood = "step 3: 4@c first of 2@a, 3@b, 4@c; step 4: 3@b first of 2@a, 3@b, 4@c; step 5: 2@a first of 2@a, 3@b, 4@c; step 6: 2@a first of 2@a, 3@b, 4@c; step 7: 3@b first of 3@b, 4@c; step 10: decision select -> 1->warn" if FormatChoices(good) != wantGood { t.Fatalf("witness %s, want %s", FormatChoices(good), wantGood) } - orders := good[0].String() + "\n" + good[1].String() + "\n" + var orders string + for _, move := range good[:len(good)-1] { + orders += move.String() + "\n" + } cases := []struct { name string lines string @@ -670,11 +675,11 @@ func TestReplayRefusesAMoveNotEnabled(t *testing.T) { {"alternative not able", "step 3: 2@a first of 2@a, 9@zzz", 1, "9@zzz is not able to act"}, {"token order where one token acts", "step 1: 1@a first of 1@a, 2@b", 1, "the run is at step 3 and step 1 had no such move"}, {"step already past", "step 1: decision select -> 1->warn", 1, "step 1 had no such move"}, - {"branch not holding", orders + "step 7: decision select -> 3->nowhere", 3, "3->nowhere is not enabled (enabled: 1->warn, 2->alarm)"}, - {"branch at the wrong place", orders + "step 7: decision elsewhere -> 1->warn", 3, "the run faced"}, - {"branch at the wrong step", orders + "step 6: decision select -> 1->warn", 3, "step 6 had no such move"}, + {"branch not holding", orders + "step 10: decision select -> 3->nowhere", 6, "3->nowhere is not enabled (enabled: 1->warn, 2->alarm)"}, + {"branch at the wrong place", orders + "step 10: decision elsewhere -> 1->warn", 6, "the run faced"}, + {"branch at the wrong step", orders + "step 9: decision select -> 1->warn", 6, "step 9 had no such move"}, {"branch where a token order is faced", "step 3: decision select -> 1->warn", 1, "must pick a token (able to act: 2@a, 3@b, 4@c)"}, - {"move left over", wantGood + "; step 99: 1@a first of 1@a, 2@b", 4, "the run ended"}, + {"move left over", wantGood + "; step 99: 1@a first of 1@a, 2@b", 7, "the run ended"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -1714,7 +1719,11 @@ func TestReplayRefusedDecisionTakesNoBranch(t *testing.T) { m := parseExploreModel(t, choiceModel) sym := m.action(t, "route") good := m.exploreAction(t, "explore", "route").Outcomes[0].Witness - witness, err := ParseChoices(good[0].String() + "\n" + good[1].String() + "\nstep 7: decision select -> 3->nowhere\n") + var orders string + for _, move := range good[:len(good)-1] { + orders += move.String() + "\n" + } + witness, err := ParseChoices(orders + "step 10: decision select -> 3->nowhere\n") if err != nil { t.Fatal(err) } @@ -1731,8 +1740,8 @@ func TestReplayRefusedDecisionTakesNoBranch(t *testing.T) { err = exec.Step() } var refused *ReplayError - if !errors.As(err, &refused) || refused.Move != 3 || !strings.Contains(refused.Faced, "3->nowhere is not enabled") { - t.Fatalf("error %v, want move 3 refused as not enabled", err) + if !errors.As(err, &refused) || refused.Move != len(good) || !strings.Contains(refused.Faced, "3->nowhere is not enabled") { + t.Fatalf("error %v, want move %d refused as not enabled", err, len(good)) } tokens := exec.Tokens() if len(tokens) != 1 { diff --git a/internal/exec/runtime/robustness_atomic_body_order_test.go b/internal/exec/runtime/robustness_atomic_body_order_test.go new file mode 100644 index 0000000000..92ed4fc7c3 --- /dev/null +++ b/internal/exec/runtime/robustness_atomic_body_order_test.go @@ -0,0 +1,1078 @@ +package runtime + +import ( + "context" + "errors" + "slices" + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" +) + +func TestRuntimeRobustnessAtomicBodyOrder(t *testing.T) { + t.Run("explore_run_budget", testAtomicBodyOrderRunBudget) + t.Run("commuting_calc_has_no_choice", testAtomicBodyOrderCommutingCalc) + t.Run("compiled_caller_falls_back_for_ordered_schedules", testAtomicBodyOrderCompiledFallback) + t.Run("compiled_cycles_propagate_reordering", testAtomicBodyOrderCompiledCycle) + t.Run("nested_calc_lists_have_order_choices", testAtomicBodyOrderNestedLists) + t.Run("fixed_policies_preserve_calc_and_action_order", testAtomicBodyOrderFixedPolicies) + t.Run("fixed_policy_orders_backward_then", testAtomicBodyOrderFixedPolicyBackwardThen) + t.Run("calc_statements_do_not_yield_to_action_body", testAtomicBodyOrderCalcDoesNotYield) + t.Run("checker_sweep_cap_is_bounded", testAtomicBodyOrderCheckerSweepCap) + t.Run("explore_sweep_cap_is_bounded", testAtomicBodyOrderExploreSweepCap) + t.Run("recursive_result_choices_are_linear", testAtomicBodyOrderRecursiveResultChoices) + t.Run("enclosing_calc_calls_do_not_reuse_result_memo", testAtomicBodyOrderEnclosingCalcMemo) + t.Run("invocation_errors_are_result_alternatives", testAtomicBodyOrderInvocationErrorResults) + t.Run("transitive_order_sensitivity_reaches_guards", testAtomicBodyOrderTransitiveGuards) + t.Run("constraint_invocation_has_one_result_choice", testAtomicBodyOrderConstraintInvocationChoice) + t.Run("nested_preview_refuses_order_dependent_invocation", testAtomicBodyOrderNestedPreviewInvocation) + t.Run("calc_flow_refusal_names_the_construct", testAtomicBodyOrderCalcFlowRefusal) + t.Run("calc_unmatched_succession_is_ignored", testAtomicBodyOrderCalcUnmatchedSuccession) + t.Run("constraint_property_finds_an_order_violation", testAtomicBodyOrderConstraintViolation) + t.Run("derived_final_orders_replay", testAtomicBodyOrderDerivedFinalReplay) + t.Run("nested_preview_refuses_order_dependent_guard", testAtomicBodyOrderNestedPreviewGuard) + t.Run("guard_refuses_external_constraint_writes", testAtomicBodyOrderGuardExternalWrite) + t.Run("guard_preserves_error_as_an_outcome", testAtomicBodyOrderGuardErrorOutcome) + t.Run("false_guard_witness_replays", testAtomicBodyOrderFalseGuardReplay) + t.Run("recursive_invocation_preserves_typed_budget_error", testAtomicBodyOrderRecursiveBudget) + t.Run("ordinary_guard_preview_error_is_left_to_firing", testAtomicBodyOrderOrdinaryGuardPreviewError) + t.Run("order_dependent_guard_preview_still_fails_dispatch", testAtomicBodyOrderOrderDependentGuardPreview) +} + +func testAtomicBodyOrderOrdinaryGuardPreviewError(t *testing.T) { + exec, err := runAtomicBodyOrderStateDispatch(t, "declared", "4 / divisor > 0", "") + if !errors.Is(err, ErrDivisionByZero) { + t.Fatalf("dispatch error = %v, want ErrDivisionByZero from firing b's guard", err) + } + if errors.Is(err, ErrOrderDependentPreview) { + t.Fatalf("dispatch error = %v, want the ordinary firing error rather than a preview refusal", err) + } + if !strings.Contains(err.Error(), "fire transition out of b1") { + t.Fatalf("dispatch error = %v, want b's guard error from its firing", err) + } + if !containsState(exec.GetStateVisits(), "a2") || containsState(exec.GetStateVisits(), "b2") { + t.Fatalf("state visits = %v, want a to fire and b to remain in b1", exec.GetStateVisits()) + } + if got := intValue(t, exec.StateData(), "divisor"); got != 0 { + t.Errorf("divisor = %d, want a's effect to set it to zero", got) + } + if got := intValue(t, exec.StateData(), "bFired"); got != 0 { + t.Errorf("bFired = %d, want b's transition effect not to run after its firing guard fails", got) + } +} + +func testAtomicBodyOrderOrderDependentGuardPreview(t *testing.T) { + constraint := `constraint def Ok { + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y == 12 + }` + exec, err := runAtomicBodyOrderStateDispatch(t, "seed:1", "Ok()", constraint) + if !errors.Is(err, ErrOrderDependentPreview) { + t.Fatalf("dispatch error = %v, want ErrOrderDependentPreview", err) + } + if !containsState(exec.GetStateVisits(), "a2") { + t.Fatalf("dispatch error = %v, state visits = %v, want a to fire before b's preview refusal", err, exec.GetStateVisits()) + } + if got := intValue(t, exec.StateData(), "divisor"); got != 0 { + t.Errorf("divisor = %d, want a's effect to run before b's preview refusal", got) + } +} + +func runAtomicBodyOrderStateDispatch(t *testing.T, schedule, guard, constraint string) (*StateExecutor, error) { + t.Helper() + source := `package test { + private import ScalarValues::*; + item def Go; + ` + constraint + ` + state Machine { + attribute divisor : Integer = 1; + attribute bFired : Integer = 0; + entry action { send new Go() to Machine; } then work; + state work parallel { + state a { + entry; then a1; + state a1; + state a2; + transition first a1 accept Go do assign divisor := 0 then a2; + } + state b { + entry; then b1; + state b1; + state b2; + transition first b1 accept Go if ` + guard + ` do assign bFired := 1 then b2; + } + } + } + }` + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, source)) + machine := findSymbolByName(idx.DocumentRoot(""), "Machine", ast.DefState) + if machine == nil { + t.Fatal("state machine Machine not found") + } + mustSchedule(t, ctx, mustPolicy(t, schedule)) + exec, err := ctx.CreateStateExecutor(machine) + if err != nil { + t.Fatalf("create state machine: %v", err) + } + return exec, exec.RunToCompletion() +} + +func testAtomicBodyOrderRunBudget(t *testing.T) { + m := conformanceModel(t, "calc_explore_statement_order") + x := m.exploreAction(t, "explore:runs=1", "Use") + if x.Complete() || x.Runs != 1 || !slices.Contains(x.BudgetsHit, "runs") { + t.Fatalf("exploration %s after %d runs, want the one-run budget hit", x.Status(), x.Runs) + } +} + +func testAtomicBodyOrderCommutingCalc(t *testing.T) { + m := conformanceModel(t, "calc_explore_statement_order_commuting") + x := m.exploreAction(t, "explore", "Use") + if !x.Complete() || x.Runs != 1 || len(x.Outcomes) != 1 { + t.Fatalf("exploration %s after %d runs, want one complete run and outcome", x.Status(), x.Runs) + } + if got := outcomeValue(t, x.Outcomes[0].Outcome, "r"); got != "5" { + t.Fatalf("r = %s, want 5", got) + } + if len(x.Outcomes[0].Witness) != 0 { + t.Fatalf("commuting calc recorded choice points: %v", x.Outcomes[0].Witness) + } +} + +func testAtomicBodyOrderCompiledFallback(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Ord { + return : Integer; + if true { + return : Integer = 12; + } else { + return : Integer = 13; + } + if true { + return : Integer = 30; + } else { + return : Integer = 31; + } + } + calc def Caller { + return : Integer; + return : Integer = Ord(); + } + action def Use { + out attribute r : Integer := 0; + assign r := Caller(); + } + }` + m := parseLibraryModel(t, source) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + ctx.SetCalcCompile(true) + callerSymbol := namedOrFoundSymbol( + t, m.idx, "test::Caller", m.idx.DocumentRoot(m.path), ast.DefCalc, ast.UsageCalc, + ) + caller, err := ctx.calcShapeOf(callerSymbol) + if err != nil { + t.Fatal(err) + } + compiled := ctx.compiledCalcOf(caller) + if compiled == nil || !ctx.reordersTransitively(callerSymbol) { + t.Fatalf("compiled caller = %#v (ineligible: %q), transitively reordered = %t; want an eligible compiled body bypassed by runtime analysis", + compiled, caller.ineligibleWhy, ctx.reordersTransitively(callerSymbol)) + } + policy := mustPolicy(t, "explore") + x, err := ExploreWith(context.Background(), policy, 1, func(int) (*Context, error) { + fresh, err := m.fresh() + if err == nil { + fresh.SetCalcCompile(true) + } + return fresh, err + }, actionRun(t, m.idx, m.path, "Use")) + if err != nil { + t.Fatal(err) + } + values := featureValues(t, x, "r") + slices.Sort(values) + if !x.Complete() || x.Runs != 2 || !slices.Equal(values, []string{"12", "30"}) { + t.Fatalf("compiled caller exploration %s after %d runs: %v, want both results in two runs", + x.Status(), x.Runs, outcomeTexts(x)) + } + for _, outcome := range x.Outcomes { + if len(outcome.Witness) != 1 || outcome.Witness[0].Kind != ChoiceStatementOrder { + t.Fatalf("compiled caller witness = %v, want one result-level statement-order choice", outcome.Witness) + } + } +} + +func testAtomicBodyOrderCompiledCycle(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Even { + in n : Integer; + return : Integer; + if n <= 0 { return : Integer = 1; } + if n <= 0 { return : Integer = 2; } + return : Integer = Odd(n - 1); + } + calc def Odd { + in n : Integer; + return : Integer = Even(n - 1); + } + action def Use { + out attribute r : Integer := 0; + assign r := Even(1); + } + }` + m := parseLibraryModel(t, source) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + ctx.SetCalcCompile(true) + evenSymbol := namedOrFoundSymbol( + t, m.idx, "test::Even", m.idx.DocumentRoot(m.path), ast.DefCalc, ast.UsageCalc, + ) + oddSymbol := namedOrFoundSymbol( + t, m.idx, "test::Odd", m.idx.DocumentRoot(m.path), ast.DefCalc, ast.UsageCalc, + ) + even, err := ctx.calcShapeOf(evenSymbol) + if err != nil { + t.Fatal(err) + } + odd, err := ctx.calcShapeOf(oddSymbol) + if err != nil { + t.Fatal(err) + } + compiledEven := ctx.compiledCalcOf(even) + compiledOdd := ctx.compiledCalcOf(odd) + if compiledEven == nil || compiledOdd == nil || + !ctx.reordersTransitively(evenSymbol) || !ctx.reordersTransitively(oddSymbol) { + t.Fatalf("compiled cycle = (%#v, %#v), transitive reordering = (%t, %t); want eligible bodies and cycle-safe analysis", + compiledEven, compiledOdd, ctx.reordersTransitively(evenSymbol), ctx.reordersTransitively(oddSymbol)) + } + mustSchedule(t, ctx, mustPolicy(t, "seed:1")) + outputs, err := ctx.ExecuteAction(m.action(t, "Use")) + if err != nil { + t.Fatal(err) + } + if got := outcomeValue(t, ctx.ActionOutcome(outputs), "r"); !slices.Contains([]string{"1", "2"}, got) { + t.Fatalf("recursive result = %s, want an admitted statement order", got) + } + if !slices.ContainsFunc(ctx.Choices(), func(choice ChoicePoint) bool { + return choice.Kind == ChoiceStatementOrder + }) { + t.Fatalf("compiled cycle did not fall back to ordered execution: %v", ctx.Choices()) + } +} + +func testAtomicBodyOrderNestedLists(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Nested { + return : Integer; + attribute y : Integer := 1; + if true { + assign y := y * 10; + assign y := y + 2; + } + attribute i : Integer := 0; + while i < 1 { + assign y := y * 10; + assign y := y + 2; + assign i := i + 1; + } + y + } + action def Use { + out attribute r : Integer := 0; + assign r := Nested(); + } + }` + m := parseLibraryModel(t, source) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, "seed:1")) + outputs, err := ctx.ExecuteAction(m.action(t, "Use")) + if err != nil { + t.Fatal(err) + } + if got := outcomeValue(t, ctx.ActionOutcome(outputs), "r"); !slices.Contains([]string{"122", "140", "302", "320"}, got) { + t.Fatalf("nested calc r = %s, want an ordering-dependent result", got) + } + choices := 0 + for _, choice := range ctx.Choices() { + if choice.Kind == ChoiceStatementOrder { + choices++ + if !strings.HasPrefix(choice.Where, resultOrderWherePrefix) || + !slices.Equal(choice.Alternatives, []string{"122", "140", "302", "320"}) { + t.Fatalf("nested calc choice = %+v, want one result choice among [122 140 302 320]", choice) + } + } + } + if choices != 1 { + t.Fatalf("nested calc recorded %d statement-order choices, want one result choice: %v", choices, ctx.Choices()) + } +} + +func testAtomicBodyOrderFixedPolicies(t *testing.T) { + m := conformanceModel(t, "calc_explore_statement_order") + for _, policy := range []string{"default", "declared", "reverse"} { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + if policy != "default" { + mustSchedule(t, ctx, mustPolicy(t, policy)) + } + outputs, err := ctx.ExecuteAction(m.action(t, "Use")) + if err != nil { + t.Fatalf("%s calc: %v", policy, err) + } + if got := outcomeValue(t, ctx.ActionOutcome(outputs), "r"); got != "12" { + t.Errorf("%s calc r = %s, want declaration-order result 12", policy, got) + } + } + + source := `package test { + private import ScalarValues::*; + action def Ordered { + out attribute r : Integer := 0; + attribute y : Integer := 1; + first start then step; + action step { + assign y := y * 10; + assign y := y + 2; + } + then action finish { assign r := y; } + then done; + } + }` + actionModel := parseLibraryModel(t, source) + for _, policy := range []string{"declared", "reverse"} { + ctx, err := actionModel.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, policy)) + outputs, err := ctx.ExecuteAction(actionModel.action(t, "Ordered")) + if err != nil { + t.Fatalf("%s action: %v", policy, err) + } + if got := outcomeValue(t, ctx.ActionOutcome(outputs), "r"); got != "12" { + t.Errorf("%s action r = %s, want declaration-order result 12", policy, got) + } + } +} + +func testAtomicBodyOrderFixedPolicyBackwardThen(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Backward { + return : Integer; + attribute y : Integer := 1; + assign y := y + 2; + assign y := y * 10; + then y; + y + } + action def Use { + out attribute r : Integer := 0; + assign r := Backward(); + } + }` + m := parseLibraryModel(t, source) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + sym := namedOrFoundSymbol( + t, m.idx, "test::Backward", m.idx.DocumentRoot(m.path), ast.DefCalc, ast.UsageCalc, + ) + shape, err := ctx.calcShapeOf(sym) + if err != nil { + t.Fatal(err) + } + order, ok := shape.statementOrders.Load(&shape.Steps[0]) + if !ok || !order.(*lower.StatementOrder).HasReversePrecedence() { + t.Fatal("calc body did not retain the backward succession precedence") + } + for _, policy := range []string{"declared", "reverse"} { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, policy)) + outputs, err := ctx.ExecuteAction(m.action(t, "Use")) + if err != nil { + t.Fatalf("%s calc: %v", policy, err) + } + if got := outcomeValue(t, ctx.ActionOutcome(outputs), "r"); got != "30" { + t.Errorf("%s calc r = %s, want the fixed stable order result 30", policy, got) + } + } +} + +func testAtomicBodyOrderCalcDoesNotYield(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Ord { + return : Integer; + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y + } + action def Use { + out attribute r : Integer := 0; + attribute marker : Integer := 0; + first start; + fork split; + action compute { assign r := Ord(); } + action observe { assign marker := marker + 1; } + join sync; + done; + succession first start then split; + succession first split then compute; + succession first split then observe; + succession first compute then sync; + succession first observe then sync; + succession first sync then done; + } + }` + m := parseLibraryModel(t, source) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, "seed:1")) + trace := NewTraceRecorder() + ctx.SetTrace(trace) + outputs, err := ctx.ExecuteAction(m.action(t, "Use")) + if err != nil { + t.Fatal(err) + } + if got := outcomeValue(t, ctx.ActionOutcome(outputs), "r"); !slices.Contains([]string{"12", "30"}, got) { + t.Fatalf("calc result = %s, want an admitted statement order", got) + } + record := trace.String() + enter := strings.Index(record, "enter calc test::Ord") + exit := strings.Index(record, "exit calc test::Ord") + interleaved := strings.Index(record, "stmt assign marker") + if enter < 0 || exit < enter || interleaved > enter && interleaved < exit { + t.Fatalf("calc statements were divided by the yielding action body:\n%s", record) + } +} + +func testAtomicBodyOrderCheckerSweepCap(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Many { + return : Boolean; + attribute a : Integer := 0; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + true + } + action def A { out attribute r : Boolean := false; assign r := false; } + }` + m := parseLibraryModel(t, source) + expr, ok := parser.ParseOneExpression(m.path, "test::Many()") + if !ok { + t.Fatal("parse calc invocation") + } + scope := m.idx.DocumentRoot(m.path) + prop := CheckProperty{ + Name: "many", + Holds: func(ctx *Context, _ *Invocation) (bool, error) { + _, err := ctx.EvalWithScope(expr, scope) + return err == nil, err + }, + } + report := checkStart(t, m, starterOf(m.action(t, "A")), CheckOptions{}, prop) + if report.Verdict == CheckExhaustive || !slices.Contains(report.BoundsHit, BoundStatementOrders) { + t.Fatalf("check %s with violations %v, want a non-exhaustive statement-order bound", report.Status(), report.Violations) + } +} + +func testAtomicBodyOrderExploreSweepCap(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Many { + return : Boolean; + attribute a : Integer := 0; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + assign a := a + 1; + true + } + action def A { out attribute r : Boolean := false; assign r := Many(); } + }` + m := parseLibraryModel(t, source) + x := m.exploreAction(t, "explore", "A") + if x.Complete() || !slices.Contains(x.BudgetsHit, BoundStatementOrders) { + t.Fatalf("exploration %s, want an incomplete statement-orders bound", x.Status()) + } + if !strings.Contains(x.Status(), "statement orders budget 1024") { + t.Fatalf("status %q, want the statement-orders limit", x.Status()) + } +} + +func testAtomicBodyOrderRecursiveResultChoices(t *testing.T) { + for _, name := range []string{ + "calc_explore_statement_order", + "calc_explore_statement_order_recursive", + "calc_explore_statement_order_recursive_deep", + } { + t.Run(name, func(t *testing.T) { + m := conformanceModel(t, name) + x := m.exploreAction(t, "explore", "Use") + if !x.Complete() || x.Runs != 2 || len(x.Outcomes) != 2 { + t.Fatalf("exploration %s after %d runs reached %v, want two complete runs", + x.Status(), x.Runs, outcomeTexts(x)) + } + got := featureValues(t, x, "r") + slices.Sort(got) + want := []string{"12", "30"} + if strings.Contains(name, "recursive") { + want = []string{"2", "20"} + } + if !slices.Equal(got, want) { + t.Fatalf("results = %v, want %v", got, want) + } + for _, outcome := range x.Outcomes { + if len(outcome.Witness) != 1 || outcome.Witness[0].Kind != ChoiceStatementOrder || + !slices.Equal(outcome.Witness[0].Among, want) { + t.Fatalf("witness = %v, want one result choice among %v", outcome.Witness, want) + } + recorded, err := ParseChoices(FormatChoices(outcome.Witness)) + if err != nil { + t.Fatalf("parse result witness: %v", err) + } + replayed, taken, err := replayed(t, m.fresh, caseRun(t, m, "Use"), recorded) + if err != nil { + t.Fatalf("replay result witness: %v", err) + } + if got := outcomeValue(t, replayed, "r"); got != outcomeValue(t, outcome.Outcome, "r") { + t.Fatalf("replay returned r = %s, want %s", got, outcomeValue(t, outcome.Outcome, "r")) + } + if FormatChoices(taken) != FormatChoices(outcome.Witness) { + t.Fatalf("replay took %q, want %q", FormatChoices(taken), FormatChoices(outcome.Witness)) + } + } + }) + } + + m := conformanceModel(t, "calc_explore_statement_order") + x := m.exploreAction(t, "explore", "Use") + if len(x.Outcomes) == 0 || len(x.Outcomes[0].Witness) != 1 { + t.Fatalf("exploration witness = %v, want a result choice", x.Outcomes) + } + bad := slices.Clone(x.Outcomes[0].Witness) + bad[0].Among = []string{"12", "unreachable"} + bad[0].Took = "unreachable" + _, _, err := replayed(t, m.fresh, caseRun(t, m, "Use"), bad) + if !errors.Is(err, ErrReplayRefused) { + t.Fatalf("unproduced result replay error = %v, want ErrReplayRefused", err) + } +} + +func testAtomicBodyOrderInvocationErrorResults(t *testing.T) { + m := parseLibraryModel(t, `package test { + private import ScalarValues::*; + calc def MayFail { + return : Real; + attribute y : Real := 1.0; + assign y := 10.0 / y; + assign y := 0.0; + y + } + action def Use { + out attribute r : Real := 0.0; + assign r := MayFail(); + } + }`) + x := m.exploreAction(t, "explore", "Use") + if !x.Complete() || x.Runs != 2 || len(x.Outcomes) != 2 { + t.Fatalf("exploration %s after %d runs: %v, want value and error results", + x.Status(), x.Runs, outcomeTexts(x)) + } + value, failed := false, false + for _, outcome := range x.Outcomes { + if len(outcome.Witness) != 1 { + t.Fatalf("invocation witness = %v, want one result choice", outcome.Witness) + } + choice := outcome.Witness[0] + if choice.Kind != ChoiceStatementOrder || len(choice.Among) != 2 || + choice.Among[0] != "0.0" || !strings.HasPrefix(choice.Among[1], "error:") { + t.Fatalf("invocation choice = %+v, want value followed by its distinct error", choice) + } + if outcome.Outcome.Err != nil { + failed = errors.Is(outcome.Outcome.Err, ErrDivisionByZero) + if !failed { + t.Fatalf("error result = %v, want ErrDivisionByZero", outcome.Outcome.Err) + } + continue + } + value = outcomeValue(t, outcome.Outcome, "r") == "0.0" + } + if !value || !failed { + t.Fatalf("invocation results have value=%t, division error=%t; want both", value, failed) + } +} + +func testAtomicBodyOrderEnclosingCalcMemo(t *testing.T) { + m := parseLibraryModel(t, `package test { + private import ScalarValues::*; + calc def Wrapper { return : Integer = 0; } + action def Use { + attribute local : Integer := 1; + calc def Read { + in n : Integer; + return : Integer; + attribute y : Integer := local; + assign y := y * 10; + assign y := y + n; + y + } + } + }`) + wrapper := namedOrFoundSymbol(t, m.idx, "test::Wrapper", m.idx.DocumentRoot(m.path), ast.DefCalc, ast.UsageCalc) + reader := namedOrFoundSymbol(t, m.idx, "test::Use::Read", m.idx.DocumentRoot(m.path), ast.DefCalc, ast.UsageCalc) + policy := mustPolicy(t, "explore") + exploration, err := ExploreWith(context.Background(), policy, 1, func(int) (*Context, error) { + return m.fresh() + }, func(ctx *Context) (Outcome, error) { + wrapperShape, err := ctx.calcShapeOf(wrapper) + if err != nil { + return Outcome{}, err + } + readerShape, err := ctx.calcShapeOf(reader) + if err != nil { + return Outcome{}, err + } + scope := m.idx.DocumentRoot(m.path) + value, err := ctx.invokeWithStatementOrderResults(wrapperShape, calcArgs{}, nil, true, func() (Value, error) { + locals := map[string]Value{"local": constInt(1)} + enclosing := []frame{mapFrame(locals)} + args := calcArgs{positional: []Value{constInt(2)}} + if _, err := ctx.invokeCalcShapeIn(readerShape, args, scope, nil, enclosing); err != nil { + return Value{}, err + } + locals["local"] = constInt(2) + return ctx.invokeCalcShapeIn(readerShape, args, scope, nil, enclosing) + }) + if err != nil { + return Outcome{}, err + } + return Outcome{Outputs: map[string]Value{"r": value}}, nil + }) + if err != nil { + t.Fatalf("explore enclosing calc calls: %v", err) + } + if !exploration.Complete() || exploration.Runs != 2 || len(exploration.Outcomes) != 2 { + t.Fatalf("exploration %s after %d runs reached %v, want two complete results", + exploration.Status(), exploration.Runs, outcomeTexts(exploration)) + } + got := featureValues(t, exploration, "r") + slices.Sort(got) + if want := []string{"22", "40"}; !slices.Equal(got, want) { + t.Fatalf("results = %v, want %v from the changed enclosing local", got, want) + } +} + +func testAtomicBodyOrderTransitiveGuards(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Ord { + return : Boolean; + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y == 12 + } + calc def Wrapper { return : Boolean; Ord() } + constraint def UsesCalc { Ord() } + action def WrapperGuard { + out attribute r : Integer := 0; + first start then choose; + decide choose; + if Wrapper() then yes; else no; + action yes { assign r := 1; } then done; + action no { assign r := 2; } then done; + } + action def ConstraintGuard { + out attribute r : Integer := 0; + first start then choose; + decide choose; + if UsesCalc() then yes; else no; + action yes { assign r := 1; } then done; + action no { assign r := 2; } then done; + } + }` + m := parseLibraryModel(t, source) + for _, name := range []string{"WrapperGuard", "ConstraintGuard"} { + t.Run(name, func(t *testing.T) { + x := m.exploreAction(t, "explore", name) + if !x.Complete() || x.Runs != 2 { + t.Fatalf("exploration %s after %d runs: %v, want both guard results", + x.Status(), x.Runs, outcomeTexts(x)) + } + got := featureValues(t, x, "r") + slices.Sort(got) + if !slices.Equal(got, []string{"1", "2"}) { + t.Fatalf("guard outcomes = %v, want both branches", got) + } + }) + } +} + +func testAtomicBodyOrderConstraintInvocationChoice(t *testing.T) { + m := conformanceModel(t, "constraint_explore_statement_order") + x := m.exploreAction(t, "explore", "A") + if !x.Complete() || x.Runs != 2 || len(x.Outcomes) != 2 { + t.Fatalf("constraint exploration %s after %d runs: %v, want two complete results", + x.Status(), x.Runs, outcomeTexts(x)) + } + for _, outcome := range x.Outcomes { + if len(outcome.Witness) != 1 { + t.Fatalf("constraint witness = %v, want one result-level choice", outcome.Witness) + } + choice := outcome.Witness[0] + if choice.Kind != ChoiceStatementOrder || choice.Step != 1 || + !strings.Contains(choice.Where, "result of constraint test::Ok") || + !slices.Equal(choice.Among, []string{"true", "false"}) { + t.Fatalf("constraint choice = %+v, want result values attributed to step 1", choice) + } + } +} + +func testAtomicBodyOrderNestedPreviewInvocation(t *testing.T) { + m := conformanceModel(t, "calc_explore_statement_order") + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, "seed:1")) + expr, ok := parser.ParseOneExpression(m.path, "test::Ord()") + if !ok { + t.Fatal("parse calc invocation") + } + restore := ctx.beginProbe() + defer restore() + _, err = ctx.EvalWithScope(expr, m.idx.DocumentRoot(m.path)) + if !errors.Is(err, ErrOrderDependentPreview) { + t.Fatalf("invocation error = %v, want ErrOrderDependentPreview", err) + } + if !strings.Contains(err.Error(), "Ord") || !strings.Contains(err.Error(), "statement orders") || + strings.Contains(err.Error(), "*ast.") { + t.Fatalf("preview error %q does not name the invocation and body", err) + } +} + +func testAtomicBodyOrderCalcFlowRefusal(t *testing.T) { + m := parseLibraryModel(t, `package test { + private import ScalarValues::*; + calc def Bad { return : Integer; first start; 1 } + }`) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + expr, ok := parser.ParseOneExpression(m.path, "test::Bad()") + if !ok { + t.Fatal("parse calc invocation") + } + _, err = ctx.EvalWithScope(expr, m.idx.DocumentRoot(m.path)) + if !errors.Is(err, ErrStatementNotExecutable) { + t.Fatalf("error = %v, want ErrStatementNotExecutable", err) + } + if !strings.Contains(err.Error(), "`first` statement") || strings.Contains(err.Error(), "*ast.") { + t.Fatalf("error %q does not name the unsupported construct", err) + } +} + +func testAtomicBodyOrderCalcUnmatchedSuccession(t *testing.T) { + m := parseLibraryModel(t, `package test { + private import ScalarValues::*; + calc def Bad { + return : Integer; + attribute x : Integer := 1; + succession first missing then x; + x + } + }`) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + expr, ok := parser.ParseOneExpression(m.path, "test::Bad()") + if !ok { + t.Fatal("parse calc invocation") + } + value, err := ctx.EvalWithScope(expr, m.idx.DocumentRoot(m.path)) + if err != nil { + t.Fatalf("evaluation error = %v, want unmatched succession ignored", err) + } + if value.Const.Int != 1 { + t.Fatalf("Bad() = %s, want 1", FormatTraceValue(value)) + } +} + +func testAtomicBodyOrderConstraintViolation(t *testing.T) { + source := `package test { + private import ScalarValues::*; + constraint def Ok { + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y == 12 + } + action def A { out attribute r : Boolean := false; assign r := false; } + }` + m := parseLibraryModel(t, source) + constraint := namedOrFoundSymbol( + t, m.idx, "test::Ok", m.idx.DocumentRoot(m.path), ast.DefConstraint, ast.UsageConstraint, + ) + prop := CheckProperty{ + Name: "ok", + Holds: func(ctx *Context, _ *Invocation) (bool, error) { + holds, err := ctx.EvaluateConstraint(constraint, constraint.OwnerScope) + if errors.Is(err, ErrViolated) { + return false, nil + } + return holds, err + }, + } + report := checkStart(t, m, starterOf(m.action(t, "A")), CheckOptions{}, prop) + if report.Verdict != CheckViolation || !slices.ContainsFunc(report.Violations, func(v Violation) bool { + return v.Kind == ViolationProperty && v.Name == "ok" + }) { + t.Fatalf("check %s with violations %v, want a violation of ok", report.Status(), report.Violations) + } +} + +func testAtomicBodyOrderDerivedFinalReplay(t *testing.T) { + m := conformanceModel(t, "calc_explore_statement_order_derived") + report := checkModel(t, m, "Use", CheckBudget{}, CheckOptions{Diverge: []string{"r"}}) + if report.Verdict != CheckDivergent { + t.Fatalf("check %s, want divergent r outcomes", report.Status()) + } + seen := map[string]bool{} + for _, final := range report.Finals { + value := final.Values["r"] + seen[value] = true + replayed := replayWitness(t, m, starterOf(m.action(t, "Use")), final.Witness, final.Outcome) + got, err := replayed.FinalValue("r") + if err != nil { + t.Errorf("replay final value: %v", err) + continue + } + if got != value { + t.Errorf("replayed r = %s, want final variant %s", got, value) + } + } + if !seen["12"] || !seen["30"] || len(seen) != 2 { + t.Fatalf("check outcomes %v, want r = 12 and 30", seen) + } +} + +func testAtomicBodyOrderNestedPreviewGuard(t *testing.T) { + m := conformanceModel(t, "action_guard_statement_order") + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, "seed:1")) + guard, ok := parser.ParseOneExpression(m.path, "test::Ok()") + if !ok { + t.Fatal("parse guard") + } + restore := ctx.beginProbe() + defer restore() + _, err = ctx.guardUnderStatementOrders(guard, 1, m.idx.DocumentRoot(m.path), func() (bool, error) { + value, err := ctx.EvalWithScope(guard, m.idx.DocumentRoot(m.path)) + if err != nil { + return false, err + } + if value.Kind != ValConst || value.Const.Kind != semantics.ValBool { + return false, ErrTypeMismatch + } + return value.Const.Bool, nil + }) + if !errors.Is(err, ErrOrderDependentPreview) { + t.Fatalf("guard error = %v, want ErrOrderDependentPreview", err) + } + if !strings.Contains(err.Error(), "Ok") || strings.Contains(err.Error(), "*ast.") { + t.Fatalf("preview error %q does not name the guard and body", err) + } +} + +func testAtomicBodyOrderGuardExternalWrite(t *testing.T) { + m := parseLibraryModel(t, `package test { + private import ScalarValues::*; + attribute outside : Integer := 0; + constraint def Guard { + attribute y : Integer := 1; + if false { assign outside := outside + 1; } + assign y := y * 10; + assign y := y + 2; + y == 12 + } + }`) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, "seed:1")) + guard, ok := parser.ParseOneExpression(m.path, "test::Guard()") + if !ok { + t.Fatal("parse guard") + } + _, err = ctx.guardUnderStatementOrders(guard, 1, m.idx.DocumentRoot(m.path), func() (bool, error) { + value, err := ctx.EvalWithScope(guard, m.idx.DocumentRoot(m.path)) + if err != nil { + return false, err + } + if value.Kind != ValConst || value.Const.Kind != semantics.ValBool { + return false, ErrTypeMismatch + } + return value.Const.Bool, nil + }) + if !errors.Is(err, ErrOrderDependentGuardEffect) { + t.Fatalf("guard error = %v, want ErrOrderDependentGuardEffect", err) + } + if !strings.Contains(err.Error(), "Guard") || strings.Contains(err.Error(), "*ast.") { + t.Fatalf("guard refusal %q does not identify the guard body", err) + } +} + +func testAtomicBodyOrderGuardErrorOutcome(t *testing.T) { + m := parseLibraryModel(t, `package test { + private import ScalarValues::*; + constraint def Mixed { + attribute d : Integer := 0; + attribute x : Real := 0.0; + assign d := 1; + assign x := 1 / d; + x == 1.0 + } + action def Use { + out attribute r : Boolean := false; + first start then choose; + decide choose; + if Mixed() then yes; + else no; + action yes { assign r := true; } + then done; + action no { assign r := false; } + then done; + } + }`) + x := m.exploreAction(t, "explore", "Use") + holds, fails, hasGuardOrder, errorTaken, hasDoesNotHold := false, false, false, false, false + for _, outcome := range x.Outcomes { + for _, choice := range outcome.Witness { + if choice.Kind != ChoiceGuardOrder { + continue + } + hasGuardOrder = true + if len(choice.Among) != 2 || choice.Among[0] != "holds" || !strings.HasPrefix(choice.Among[1], "error:") { + t.Errorf("guard alternatives %v, want holds followed by the distinct error", choice.Among) + } + errorTaken = errorTaken || strings.HasPrefix(choice.Took, "error:") + hasDoesNotHold = hasDoesNotHold || slices.Contains(choice.Among, "does not hold") + } + if outcome.Outcome.Err != nil { + fails = true + if !strings.Contains(outcome.Outcome.Err.Error(), "division") { + t.Errorf("guard error = %v, want a division error", outcome.Outcome.Err) + } + continue + } + if got := outcomeValue(t, outcome.Outcome, "r"); got == "true" { + holds = true + } + } + if !holds || !fails || !hasGuardOrder || !errorTaken || hasDoesNotHold { + t.Fatalf("exploration %s has holds=%t, error=%t, guard choice=%t, error taken=%t, false result=%t; want holds and error as distinct results", + x.Status(), holds, fails, hasGuardOrder, errorTaken, hasDoesNotHold) + } +} + +func testAtomicBodyOrderFalseGuardReplay(t *testing.T) { + m := conformanceModel(t, "action_guard_statement_order") + report := checkModel(t, m, "DecisionGuard", CheckBudget{}, CheckOptions{Diverge: []string{"r"}}) + for _, final := range report.Finals { + if final.Values["r"] != "2" { + continue + } + if !slices.ContainsFunc(final.Witness.Choices, func(choice ChoiceTaken) bool { + return choice.Kind == ChoiceGuardOrder && choice.Took == "does not hold" + }) { + t.Fatalf("false-guard final witness lacks its guard choice: %s", final.Witness) + } + replayed := replayWitness(t, m, starterOf(m.action(t, "DecisionGuard")), final.Witness, final.Outcome) + if got := outcomeValue(t, replayed.Inv.Outcome(), "r"); got != "2" { + t.Fatalf("replayed false-guard outcome r = %s, want 2", got) + } + return + } + t.Fatalf("check %s has no r = 2 final", report.Status()) +} + +func testAtomicBodyOrderRecursiveBudget(t *testing.T) { + source := `package test { + private import ScalarValues::*; + calc def Recur { + return : Integer; + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + Recur() + } + }` + m := parseLibraryModel(t, source) + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, "seed:1")) + budgets := ctx.Budgets() + budgets.MaxCalcDepth = 16 + if err := ctx.SetBudgets(budgets); err != nil { + t.Fatal(err) + } + ctx.SetTrace(NewTraceRecorder()) + expr, ok := parser.ParseOneExpression(m.path, "test::Recur()") + if !ok { + t.Fatal("parse recursive calc invocation") + } + _, err = ctx.EvalWithScope(expr, m.idx.DocumentRoot(m.path)) + if !errors.Is(err, ErrCalcRecursionLimit) { + t.Fatalf("error = %v, want ErrCalcRecursionLimit", err) + } + if slices.ContainsFunc(ctx.ChoicesTaken(), func(choice ChoiceTaken) bool { + return choice.Kind == ChoiceStatementOrder + }) { + t.Fatalf("recursive invocation has one distinct error result, but recorded statement-order choices: %s", ctx.Trace()) + } +} diff --git a/internal/exec/runtime/robustness_case_step_order_test.go b/internal/exec/runtime/robustness_case_step_order_test.go new file mode 100644 index 0000000000..a3a9f6bfab --- /dev/null +++ b/internal/exec/runtime/robustness_case_step_order_test.go @@ -0,0 +1,51 @@ +package runtime + +import ( + "context" + "slices" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +func TestRuntimeRobustnessCaseStepOrder(t *testing.T) { + t.Run("explore_budget_is_incomplete", func(t *testing.T) { + source := `package test { + analysis def ManySteps { + return : Integer; + attribute x : Integer := 0; + action s1 { assign x := x + 1; } + action s2 { assign x := x + 1; } + action s3 { assign x := x + 1; } + action s4 { assign x := x + 1; } + action s5 { assign x := x + 1; } + action s6 { assign x := x + 1; } + action s7 { assign x := x + 1; } + x + } + }` + m := parseLibraryModel(t, source) + sym := namedOrFoundSymbol(t, m.idx, "test::ManySteps", m.idx.DocumentRoot(m.path), ast.DefAnalysisCase, ast.UsageAnalysisCase) + policy, err := ExplorePolicy(DefaultExploreBudget) + if err != nil { + t.Fatal(err) + } + exploration, err := Explore(context.Background(), policy, m.fresh, func(ctx *Context) (Outcome, error) { + result, err := ctx.RunAnalysis(sym, AnalysisArgs{}, m.idx.DocumentRoot(m.path), nil) + if err != nil { + return Outcome{}, err + } + outputs := make(map[string]Value, len(result.Outputs)) + for _, output := range result.Outputs { + outputs[output.Name] = output.Value + } + return Outcome{Outputs: outputs}, nil + }) + if err != nil { + t.Fatalf("explore many-step case: %v", err) + } + if exploration.Complete() || !slices.Contains(exploration.BudgetsHit, "runs") { + t.Fatalf("exploration %s after %d runs, want incomplete run-budget status", exploration.Status(), exploration.Runs) + } + }) +} diff --git a/internal/exec/runtime/robustness_constraint_body_steps_test.go b/internal/exec/runtime/robustness_constraint_body_steps_test.go new file mode 100644 index 0000000000..a9a9aeca46 --- /dev/null +++ b/internal/exec/runtime/robustness_constraint_body_steps_test.go @@ -0,0 +1,256 @@ +package runtime + +import ( + "errors" + "strings" + "testing" +) + +// TestRuntimeRobustnessConstraintBodySteps covers the failure and isolation modes of +// the steps a constraint body performs: a write reaching outside the body's own +// performance is refused and leaves the world it would have written unchanged, a +// parameter written is the performance's copy and binds nothing back, two checks of +// one body share no state, and a loop in the body spends the run's step budget. +func TestRuntimeRobustnessConstraintBodySteps(t *testing.T) { + t.Run("a refused write of the constrained object leaves it unchanged", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + part def Rig { attribute z : Real = 1; constraint writes { assign z := 99; z > 0 } } + }` + ctx, idx := contextForSource(t, src) + rig := lookupOne(t, idx, "test::Rig") + feat := featureNamed(ctx, rig, "writes") + if feat == nil || feat.Symbol == nil { + t.Fatal("constraint writes not found") + } + if _, err := ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil); !errors.Is(err, ErrConstraintExternalAssignment) { + t.Fatalf("err = %v, want ErrConstraintExternalAssignment", err) + } + same, err := evalIn(t, ctx, rig.Scope, "z == 1") + if err != nil || !(same.isBool() && same.Const.Bool) { + t.Fatalf("z == 1 after the refused write = %v, %v: the refused write changed the constrained feature", FormatValue(same), err) + } + }) + + t.Run("a refused chained write leaves the chain's object unchanged", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + part def Inner { attribute w : Real = 1; } + part def Rig { + part inner : Inner; + constraint writes { attribute v : Inner; assign v.w := 99; v.w > 0 } + } + part rig : Rig; + }` + ctx, idx := contextForSource(t, src) + rig := lookupOne(t, idx, "test::Rig") + feat := featureNamed(ctx, rig, "writes") + if feat == nil || feat.Symbol == nil { + t.Fatal("constraint writes not found") + } + if _, err := ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil); !errors.Is(err, ErrConstraintEffect) { + t.Fatalf("err = %v, want ErrConstraintEffect", err) + } + same, err := evalIn(t, ctx, lookupOne(t, idx, "test").Scope, "rig.inner.w == 1") + if err != nil || !(same.isBool() && same.Const.Bool) { + t.Fatalf("rig.inner.w == 1 after the refused write = %v, %v: the refused write changed the chained object", FormatValue(same), err) + } + }) + + t.Run("a parameter written binds nothing back to the check's binding", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + part def Rig { attribute z : Real = 3; } + part rig : Rig; + constraint def Moves { + in p : Real; + assign p := p + 10; + p > 10 + } + constraint moved : Moves { in p = rig.z; } + }` + ctx, idx := contextForSource(t, src) + moved := lookupOne(t, idx, "test::moved") + satisfied, err := ctx.EvaluateConstraintOn(moved, moved.OwnerScope, nil) + if err != nil { + t.Fatalf("err = %v", err) + } + if !satisfied { + t.Error("the parameter's copy took the step's write, and the condition holds") + } + same, err := evalIn(t, ctx, lookupOne(t, idx, "test").Scope, "rig.z == 3") + if err != nil || !(same.isBool() && same.Const.Bool) { + t.Fatalf("rig.z == 3 after the parameter write = %v, %v: the parameter write reached the bound feature", FormatValue(same), err) + } + }) + + t.Run("two checks of one body share no state", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + constraint def Counted { + attribute n : Real = 0; + assign n := n + 1; + n == 1 + } + }` + ctx, idx := contextForSource(t, src) + counted := lookupOne(t, idx, "test::Counted") + for i := 0; i < 2; i++ { + satisfied, err := ctx.EvaluateConstraint(counted, counted.OwnerScope) + if err != nil { + t.Fatalf("check %d: err = %v", i, err) + } + if !satisfied { + t.Fatalf("check %d: not satisfied — the body carried state between evaluations", i) + } + } + }) + + t.Run("a loop spends the check's step budget", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + constraint def Spins { + attribute i : Real = 0; + while true { assign i := i + 1; } + i > 0 + } + }` + ctx, idx := contextForSource(t, src) + spins := lookupOne(t, idx, "test::Spins") + _, err := ctx.EvaluateConstraint(spins, spins.OwnerScope) + if !errors.Is(err, ErrStepLimitExceeded) { + t.Fatalf("err = %v, want ErrStepLimitExceeded", err) + } + }) + + t.Run("a nested require body runs its own steps", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + requirement safing { + attribute margin : Real = 0; + require constraint { attribute m : Real = 0; assign m := margin + 9; m > 4 } + } + }` + ctx, idx := contextForSource(t, src) + safing := lookupOne(t, idx, "test::safing") + satisfied, err := ctx.EvaluateRequirement(safing, safing.OwnerScope) + if err != nil { + t.Fatalf("err = %v", err) + } + if !satisfied { + t.Error("the nested body's steps made its condition hold, and it is not satisfied") + } + }) + + t.Run("a body stating steps but no result is refused", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + constraint def Silent { + attribute y : Real = 1; + assign y := 2; + } + }` + ctx, idx := contextForSource(t, src) + silent := lookupOne(t, idx, "test::Silent") + _, err := ctx.EvaluateConstraint(silent, silent.OwnerScope) + if !errors.Is(err, ErrNoConditions) { + t.Fatalf("err = %v, want ErrNoConditions", err) + } + if !strings.Contains(err.Error(), "no result expression") { + t.Errorf("err = %v, want it to say the body states no result expression", err) + } + }) + + t.Run("an assumption failing among steps denies nothing", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + requirement pessimistic { + attribute a : Real = 2.0; + assume constraint { attribute b : Real = 1; assign b := b + 5; b <= 3 } + require constraint { a > 0 } + } + }` + ctx, idx := contextForSource(t, src) + pessimistic := lookupOne(t, idx, "test::pessimistic") + satisfied, err := ctx.EvaluateRequirement(pessimistic, pessimistic.OwnerScope) + if err != nil { + t.Fatalf("err = %v", err) + } + if !satisfied { + t.Error("a false assumption is trusted, not a violation — the requirement should hold") + } + }) + + t.Run("a negated body evaluates its steps before negating", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + part def Rig { + assert not constraint denied { attribute w : Real = 0; assign w := 101; w > 100 } + } + }` + ctx, idx := contextForSource(t, src) + rig := lookupOne(t, idx, "test::Rig") + feat := featureNamed(ctx, rig, "denied") + if feat == nil || feat.Symbol == nil { + t.Fatal("constraint denied not found") + } + satisfied, err := ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil) + var violation *ViolationError + if !errors.As(err, &violation) { + t.Fatalf("err = %v, want a *ViolationError: the steps made the condition hold, so the negated assertion fails", err) + } + if satisfied { + t.Error("the negated assertion holds while the condition its steps made true does") + } + }) + + t.Run("a nested body's steps read the outer locals", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + part def Rig { + constraint outerRead { + attribute x : Integer = 0; + assign x := 5; + assert constraint { if x == 5 { } x == 5 } + } + } + }` + ctx, idx := contextForSource(t, src) + rig := lookupOne(t, idx, "test::Rig") + feat := featureNamed(ctx, rig, "outerRead") + if feat == nil || feat.Symbol == nil { + t.Fatal("constraint outerRead not found") + } + satisfied, err := ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil) + if err != nil { + t.Fatalf("err = %v", err) + } + if !satisfied { + t.Error("the nested body's steps must read the outer body's local x, and the condition holds") + } + }) + + t.Run("a nested body's write of an outer local is refused", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + part def Rig { + attribute z : Real = 1; + constraint outerWrite { + attribute x : Integer = 0; + assign x := 5; + assert constraint { assign x := 9; x == 9 } + } + } + }` + ctx, idx := contextForSource(t, src) + rig := lookupOne(t, idx, "test::Rig") + feat := featureNamed(ctx, rig, "outerWrite") + if feat == nil || feat.Symbol == nil { + t.Fatal("constraint outerWrite not found") + } + _, err := ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil) + if !errors.Is(err, ErrConstraintExternalAssignment) { + t.Fatalf("err = %v, want ErrConstraintExternalAssignment: a nested constraint is its own performance", err) + } + }) +} diff --git a/internal/exec/runtime/robustness_explore_body_interleavings_test.go b/internal/exec/runtime/robustness_explore_body_interleavings_test.go new file mode 100644 index 0000000000..3db88ca477 --- /dev/null +++ b/internal/exec/runtime/robustness_explore_body_interleavings_test.go @@ -0,0 +1,363 @@ +package runtime + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" +) + +// TestRuntimeRobustnessExploreBodyInterleavings exercises the scheduler boundaries +// inside a leaf body: another performance may run between a body's start shot and +// its assignment, and every surface that enumerates or replays schedules reaches it. +func TestRuntimeRobustnessExploreBodyInterleavings(t *testing.T) { + t.Run("explore_reaches_every_admitted_outcome", testBodyInterleavingsExplored) + t.Run("budget_exhaustion_is_reported", testBodyInterleavingsBudgetHit) + t.Run("recorded_interleaving_replays", testBodyInterleavingsReplay) + t.Run("seeded_runs_are_reproducible_and_reach_the_lost_update", testBodyInterleavingsSeeded) + t.Run("declared_and_reverse_keep_their_results", testBodyInterleavingsFixedPolicies) + t.Run("checker_finds_every_outcome", testBodyInterleavingsChecked) + t.Run("callee_executors_interleave", testBodyInterleavingsCallees) + t.Run("callees_touching_only_their_own_features_run_whole", testBodyInterleavingsOwnCallees) + t.Run("callee_output_writes_are_observed_one_at_a_time", testBodyInterleavingsCalleeOutputs) + t.Run("a_sibling_starts_its_timer_before_a_callee_moves_the_clock", testBodyInterleavingsSiblingTimer) +} + +// caseRun runs the case's one action under ctx and reports its outcome. +func caseRun(t *testing.T, m *exploreModel, name string) func(*Context) (Outcome, error) { + t.Helper() + sym := m.action(t, name) + return func(ctx *Context) (Outcome, error) { + outputs, err := ctx.ExecuteAction(sym) + if err != nil { + return Outcome{}, err + } + return ctx.ActionOutcome(outputs), nil + } +} + +// featureValues spells feature in each outcome, in outcome order. +func featureValues(t *testing.T, x *Exploration, feature string) []string { + t.Helper() + var values []string + for _, o := range x.Outcomes { + values = append(values, outcomeValue(t, o.Outcome, feature)) + } + return values +} + +func outcomeValue(t *testing.T, o Outcome, feature string) string { + t.Helper() + if o.Err != nil { + t.Fatalf("outcome is an error: %v", o.Err) + } + v, ok := o.Outputs[feature] + if !ok { + t.Fatalf("outcome %s has no %s", o, feature) + } + return FormatValue(v) +} + +func testBodyInterleavingsExplored(t *testing.T) { + for _, c := range []struct { + fixture, action, feature string + want []string + }{ + {"action_explore_body_lost_update", "Race", "c", []string{"1", "2"}}, + {"action_explore_body_three_way", "Race3", "c", []string{"1", "2", "3"}}, + {"action_explore_body_fork_lost_update", "ForkPlain", "c", []string{"1", "2"}}, + {"action_explore_body_ordered_substeps", "Ordered", "log", []string{`"12b"`, `"1b2"`, `"b12"`}}, + {"action_step_multiplicity_single_assignment", "Single", "c", []string{"3"}}, + {"action_explore_body_guard_branch", "GuardBranch", "c", []string{"1", "2"}}, + } { + t.Run(c.action, func(t *testing.T) { + x := conformanceModel(t, c.fixture).exploreAction(t, "explore", c.action) + if !x.Complete() { + t.Fatalf("exploration %s, want complete", x.Status()) + } + got := featureValues(t, x, c.feature) + slices.Sort(got) + got = slices.Compact(got) + if !slices.Equal(got, c.want) { + t.Fatalf("%s over every schedule = %v, want %v", c.feature, got, c.want) + } + }) + } +} + +func testBodyInterleavingsBudgetHit(t *testing.T) { + m := conformanceModel(t, "action_explore_body_three_way") + x := m.exploreAction(t, "explore:runs=3", "Race3") + if x.Complete() || !slices.Contains(x.BudgetsHit, "runs") || x.Runs != 3 { + t.Fatalf("exploration %s after %d runs, want the runs budget of 3 hit", x.Status(), x.Runs) + } + if !strings.Contains(x.Status(), "runs budget 3") { + t.Errorf("status %q does not name the runs budget", x.Status()) + } + x = m.exploreAction(t, "explore:depth=1", "Race3") + if x.Complete() || !slices.Contains(x.BudgetsHit, "depth") { + t.Fatalf("exploration %s, want the depth budget hit", x.Status()) + } + if !strings.Contains(x.Status(), "depth budget 1") { + t.Errorf("status %q does not name the depth budget", x.Status()) + } +} + +func testBodyInterleavingsReplay(t *testing.T) { + m := conformanceModel(t, "action_explore_body_lost_update") + run := caseRun(t, m, "Race") + x := m.exploreAction(t, "explore", "Race") + i := slices.IndexFunc(x.Outcomes, func(o ExploredOutcome) bool { return outcomeValue(t, o.Outcome, "c") == "1" }) + if i < 0 { + t.Fatalf("exploration reached %v, want the lost update c = 1", outcomeTexts(x)) + } + witness := x.Outcomes[i].Witness + recorded := FormatChoices(witness) + parsed, err := ParseChoices(recorded) + if err != nil { + t.Fatalf("parse recorded witness %q: %v", recorded, err) + } + outcome, taken, err := replayed(t, m.fresh, run, parsed) + if err != nil { + t.Fatalf("replay %q: %v", recorded, err) + } + if got := outcomeValue(t, outcome, "c"); got != "1" { + t.Fatalf("replaying %q gave c = %s, want 1", recorded, got) + } + if again := FormatChoices(taken); again != recorded { + t.Errorf("replay took %q, want the witness %q", again, recorded) + } +} + +func testBodyInterleavingsSeeded(t *testing.T) { + m := conformanceModel(t, "action_explore_body_lost_update") + run := caseRun(t, m, "Race") + seen := map[string]bool{} + for seed := 1; seed <= 32; seed++ { + spelling := fmt.Sprintf("seed:%d", seed) + var first string + for range 2 { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, spelling)) + outcome, err := run(ctx) + if err != nil { + t.Fatalf("%s: %v", spelling, err) + } + got := outcomeValue(t, outcome, "c") + if first == "" { + first = got + } else if got != first { + t.Fatalf("%s gave c = %s, then c = %s; want the seed to replay its run", spelling, first, got) + } + } + seen[first] = true + } + if !seen["1"] || !seen["2"] { + t.Fatalf("seeds 1..32 reached %v, want both c = 1 and c = 2", seen) + } +} + +func testBodyInterleavingsFixedPolicies(t *testing.T) { + m := conformanceModel(t, "action_explore_body_lost_update") + run := caseRun(t, m, "Race") + for _, spelling := range []string{"reverse", "declared"} { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, spelling)) + outcome, err := run(ctx) + if err != nil { + t.Fatalf("%s: %v", spelling, err) + } + if got := outcomeValue(t, outcome, "c"); got != "2" { + t.Errorf("%s gave c = %s, want 2, each body run whole", spelling, got) + } + } +} + +func testBodyInterleavingsChecked(t *testing.T) { + m := conformanceModel(t, "action_explore_body_fork_lost_update") + for _, opts := range []CheckOptions{reduced(), unreduced()} { + report, err := Check(context.Background(), m.fresh, starterOf(m.action(t, "ForkPlain")), CheckBudget{}, opts, nil) + if err != nil { + t.Fatalf("reduce=%v: %v", opts.Reduce, err) + } + if len(report.BoundsHit) != 0 { + t.Fatalf("reduce=%v: bounds hit %v, want exhaustive", opts.Reduce, report.BoundsHit) + } + if got := divergentValues(report, "c"); !slices.Equal(got, []string{"1", "2"}) { + t.Errorf("reduce=%v: c diverges over %v, want [1 2]", opts.Reduce, got) + } + } +} + +// testBodyInterleavingsCallees checks that the performances a flow invokes, each +// in an executor of its own, interleave inside their bodies under explore and check, +// and that a fixed policy still runs each whole. +func testBodyInterleavingsCallees(t *testing.T) { + for _, c := range []struct{ fixture, action string }{ + {"action_explore_body_typed_callees", "TypedCallees"}, + {"action_explore_body_performed_callees", "PerformedCallees"}, + } { + t.Run(c.action, func(t *testing.T) { + m := conformanceModel(t, c.fixture) + x := m.exploreAction(t, "explore", c.action) + if !x.Complete() { + t.Fatalf("exploration %s, want complete", x.Status()) + } + got := featureValues(t, x, "seen") + slices.Sort(got) + if got = slices.Compact(got); !slices.Equal(got, []string{"1", "2"}) { + t.Fatalf("seen over every schedule = %v, want [1 2]", got) + } + report, err := Check(context.Background(), m.fresh, starterOf(m.action(t, c.action)), CheckBudget{}, reduced(), nil) + if err != nil { + t.Fatal(err) + } + if got := divergentValues(report, "seen"); !slices.Equal(got, []string{"1", "2"}) { + t.Errorf("check: seen diverges over %v, want [1 2]", got) + } + run := caseRun(t, m, c.action) + seeded := map[string]bool{} + for seed := 1; seed <= 32; seed++ { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, fmt.Sprintf("seed:%d", seed))) + outcome, err := run(ctx) + if err != nil { + t.Fatalf("seed:%d: %v", seed, err) + } + seeded[outcomeValue(t, outcome, "seen")] = true + } + if !seeded["1"] || !seeded["2"] || len(seeded) != 2 { + t.Errorf("seeds 1-32 gave seen in %v, want both 1 and 2", seeded) + } + for _, spelling := range []string{"reverse", "declared"} { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, spelling)) + outcome, err := run(ctx) + if err != nil { + t.Fatalf("%s: %v", spelling, err) + } + if got := outcomeValue(t, outcome, "seen"); got != "2" { + t.Errorf("%s gave seen = %s, want 2, each callee run whole", spelling, got) + } + } + }) + } +} + +func testBodyInterleavingsOwnCallees(t *testing.T) { + m := conformanceModel(t, "action_explore_body_own_callees") + x := m.exploreAction(t, "explore", "OwnCallees") + if !x.Complete() { + t.Fatalf("exploration %s, want complete", x.Status()) + } + got := featureValues(t, x, "sum") + if got = slices.Compact(got); !slices.Equal(got, []string{"5"}) { + t.Fatalf("sum over every schedule = %v, want [5]", got) + } + if runs := x.Runs; runs > 2 { + t.Errorf("explore took %d runs, want at most 2: the callees share nothing", runs) + } +} + +func testBodyInterleavingsCalleeOutputs(t *testing.T) { + text, err := os.ReadFile(filepath.Join("testdata", "robustness", "action_explore_body_callee_outputs.sysml")) + if err != nil { + t.Fatal(err) + } + m := parseExploreModel(t, string(text)) + x := m.exploreAction(t, "explore", "CalleeOutputs") + if !x.Complete() { + t.Fatalf("exploration %s, want complete", x.Status()) + } + var got []string + errs := 0 + for _, o := range x.Outcomes { + if o.Outcome.Err != nil { + if !errors.Is(o.Outcome.Err, ErrNodeNotPerformed) { + t.Errorf("outcome error %v, want only %v", o.Outcome.Err, ErrNodeNotPerformed) + } + errs++ + continue + } + got = append(got, outcomeValue(t, o.Outcome, "seen")) + } + slices.Sort(got) + if !slices.Equal(got, []string{"0", "1", "2"}) || errs != 1 { + t.Fatalf("seen over every schedule = %v with %d error outcomes, want [0 1 2] and the read before producer is performed", got, errs) + } +} + +// A performed chain parking on a timer is a move: the step ends there, and the +// sibling not yet stepped starts its own timer before the clock moves. +func testBodyInterleavingsSiblingTimer(t *testing.T) { + text, err := os.ReadFile(filepath.Join("testdata", "robustness", "action_explore_body_sibling_timer.sysml")) + if err != nil { + t.Fatal(err) + } + m := parseLibraryModel(t, string(text)) + sym := m.action(t, "SiblingTimer") + clocks := map[float64]bool{} + run := func(ctx *Context) (Outcome, error) { + outputs, err := ctx.ExecuteAction(sym) + clocks[ctx.Clock().Now()] = true + if err != nil { + return Outcome{}, err + } + return ctx.ActionOutcome(outputs), nil + } + x, err := Explore(context.Background(), mustPolicy(t, "explore"), m.fresh, run) + if err != nil { + t.Fatal(err) + } + if !x.Complete() { + t.Fatalf("exploration %s, want complete", x.Status()) + } + if got := slices.Compact(featureValues(t, x, "gated")); !slices.Equal(got, []string{"1"}) { + t.Errorf("explore: gated over every schedule = %v, want [1]", got) + } + for seed := 1; seed <= 8; seed++ { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, fmt.Sprintf("seed:%d", seed))) + outcome, err := run(ctx) + if err != nil { + t.Fatalf("seed:%d: %v", seed, err) + } + if got := outcomeValue(t, outcome, "gated"); got != "1" { + t.Errorf("seed:%d gave gated = %s, want 1", seed, got) + } + } + if len(clocks) != 1 || !clocks[10] { + t.Errorf("runs ended at clocks %v, want only 10", clocks) + } + report, err := Check(context.Background(), m.fresh, starterOf(sym), CheckBudget{}, reduced(), nil) + if err != nil { + t.Fatal(err) + } + if len(report.BoundsHit) != 0 { + t.Fatalf("check: bounds hit %v, want exhaustive", report.BoundsHit) + } + for _, f := range report.Finals { + if f.Values["gated"] != "1" { + t.Errorf("check final %s, want gated = 1", f.Outcome) + } + } +} diff --git a/internal/exec/runtime/robustness_explore_statement_order_test.go b/internal/exec/runtime/robustness_explore_statement_order_test.go new file mode 100644 index 0000000000..52111e9028 --- /dev/null +++ b/internal/exec/runtime/robustness_explore_statement_order_test.go @@ -0,0 +1,200 @@ +package runtime + +import ( + "errors" + "fmt" + "slices" + "strings" + "testing" +) + +// TestRuntimeRobustnessExploreStatementOrder exercises the order of a body's direct +// statements no succession orders: every surface that enumerates or replays +// schedules reaches each order, a fixed policy keeps declaration order, and a +// search cut short by its budget says so. +func TestRuntimeRobustnessExploreStatementOrder(t *testing.T) { + t.Run("explore_reaches_every_admitted_order", testStatementOrderExplored) + t.Run("budget_exhaustion_is_incomplete", testStatementOrderBudgetHit) + t.Run("recorded_order_replays", testStatementOrderReplay) + t.Run("seeded_runs_are_reproducible_and_reach_each_order", testStatementOrderSeeded) + t.Run("declared_and_reverse_keep_declaration_order", testStatementOrderFixedPolicies) + t.Run("cyclic_calc_binding_is_a_typed_error", testStatementOrderCyclicCalcBinding) +} + +const manyStatements = `package test { + private import ScalarValues::*; + action def Many { + attribute c : Integer := 1; + first start then s; + action s { + assign c := c * 2; + assign c := c + 1; + assign c := c * 3; + assign c := c + 5; + assign c := c * 7; + } + then done; + } +}` + +func testStatementOrderExplored(t *testing.T) { + for _, c := range []struct { + fixture, action, feature string + want []string + }{ + {"action_explore_statement_order_dependent", "Order", "y", []string{"0", "1"}}, + {"action_explore_statement_order_then", "Then", "y", []string{"1"}}, + } { + t.Run(c.action, func(t *testing.T) { + x := conformanceModel(t, c.fixture).exploreAction(t, "explore", c.action) + if !x.Complete() { + t.Fatalf("exploration %s, want complete", x.Status()) + } + got := featureValues(t, x, c.feature) + slices.Sort(got) + if got = slices.Compact(got); !slices.Equal(got, c.want) { + t.Fatalf("%s over every schedule = %v, want %v", c.feature, got, c.want) + } + }) + } +} + +// testStatementOrderBudgetHit: five pairwise dependent statements have 120 orders; +// a runs budget below that ends the search incomplete, never complete over fewer. +func testStatementOrderBudgetHit(t *testing.T) { + m := parseExploreModel(t, manyStatements) + x := m.exploreAction(t, "explore:runs=8", "Many") + if x.Complete() || !slices.Contains(x.BudgetsHit, "runs") || x.Runs != 8 { + t.Fatalf("exploration %s after %d runs, want the runs budget of 8 hit", x.Status(), x.Runs) + } + if !strings.HasPrefix(x.Status(), "incomplete") || !strings.Contains(x.Status(), "runs budget 8") { + t.Errorf("status %q, want incomplete naming the runs budget", x.Status()) + } + x = m.exploreAction(t, "explore", "Many") + if !x.Complete() || x.Runs != 120 { + t.Fatalf("exploration %s after %d runs, want complete over the 120 orders", x.Status(), x.Runs) + } +} + +func testStatementOrderReplay(t *testing.T) { + m := conformanceModel(t, "action_explore_statement_order_dependent") + run := caseRun(t, m, "Order") + x := m.exploreAction(t, "explore", "Order") + i := slices.IndexFunc(x.Outcomes, func(o ExploredOutcome) bool { return outcomeValue(t, o.Outcome, "y") == "0" }) + if i < 0 { + t.Fatalf("exploration reached %v, want y = 0, read before the write", outcomeTexts(x)) + } + recorded := FormatChoices(x.Outcomes[i].Witness) + parsed, err := ParseChoices(recorded) + if err != nil { + t.Fatalf("parse recorded witness %q: %v", recorded, err) + } + outcome, taken, err := replayed(t, m.fresh, run, parsed) + if err != nil { + t.Fatalf("replay %q: %v", recorded, err) + } + if got := outcomeValue(t, outcome, "y"); got != "0" { + t.Fatalf("replaying %q gave y = %s, want 0", recorded, got) + } + if again := FormatChoices(taken); again != recorded { + t.Errorf("replay took %q, want the witness %q", again, recorded) + } +} + +func testStatementOrderSeeded(t *testing.T) { + m := conformanceModel(t, "action_explore_statement_order_dependent") + run := caseRun(t, m, "Order") + seen := map[string]bool{} + for seed := 1; seed <= 32; seed++ { + spelling := fmt.Sprintf("seed:%d", seed) + var first string + for range 2 { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, spelling)) + outcome, err := run(ctx) + if err != nil { + t.Fatalf("%s: %v", spelling, err) + } + got := outcomeValue(t, outcome, "y") + if first == "" { + first = got + } else if got != first { + t.Fatalf("%s gave y = %s, then y = %s; want the seed to replay its run", spelling, first, got) + } + } + seen[first] = true + } + if !seen["0"] || !seen["1"] { + t.Fatalf("seeds 1..32 reached %v, want both y = 0 and y = 1", seen) + } +} + +func testStatementOrderFixedPolicies(t *testing.T) { + m := conformanceModel(t, "action_explore_statement_order_dependent") + run := caseRun(t, m, "Order") + for _, spelling := range []string{"reverse", "declared"} { + ctx, err := m.fresh() + if err != nil { + t.Fatal(err) + } + mustSchedule(t, ctx, mustPolicy(t, spelling)) + outcome, err := run(ctx) + if err != nil { + t.Fatalf("%s: %v", spelling, err) + } + if got := outcomeValue(t, outcome, "y"); got != "1" { + t.Errorf("%s gave y = %s, want 1, the statements in declaration order", spelling, got) + } + } +} + +// cyclicCalcBindings binds a calc usage's input from itself, and two usages from each other. +const cyclicCalcBindings = `package test { + private import ScalarValues::*; + calc def Twice { in k : Real; out d = k * 2.0; } + action def Self { + attribute v : Real = 1.0; + attribute doubled : Real = 0.0; + first start then compute; + action compute { + calc t : Twice { in k = t.d; } + assign v := 2.0; + assign doubled := t.d; + } + then done; + } + action def Mutual { + attribute v : Real = 1.0; + attribute doubled : Real = 0.0; + first start then compute; + action compute { + calc a : Twice { in k = b.d; } + calc b : Twice { in k = a.d; } + assign v := 2.0; + assign doubled := a.d; + } + then done; + } +}` + +// testStatementOrderCyclicCalcBinding: building the statements' footprints through +// a cyclic binding terminates, and every order ends in the runtime's recursion error. +func testStatementOrderCyclicCalcBinding(t *testing.T) { + m := parseExploreModel(t, cyclicCalcBindings) + for _, action := range []string{"Self", "Mutual"} { + t.Run(action, func(t *testing.T) { + x := m.exploreAction(t, "explore", action) + if !x.Complete() || len(x.Outcomes) == 0 { + t.Fatalf("exploration %s with %v, want complete", x.Status(), outcomeTexts(x)) + } + for _, o := range x.Outcomes { + if !errors.Is(o.Outcome.Err, ErrCalcUsageRecursion) { + t.Errorf("outcome %s, want ErrCalcUsageRecursion", o.Outcome) + } + } + }) + } +} diff --git a/internal/exec/runtime/robustness_extent_model_determined_test.go b/internal/exec/runtime/robustness_extent_model_determined_test.go new file mode 100644 index 0000000000..1d926bd4d4 --- /dev/null +++ b/internal/exec/runtime/robustness_extent_model_determined_test.go @@ -0,0 +1,964 @@ +package runtime + +import ( + "errors" + "slices" + "strings" + "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" +) + +// TestRuntimeRobustnessExtentModelDetermined covers the failure and determinism modes of +// the namespace-owned usages an extent reaches: a binding connector between objects must +// refuse ends whose values differ or that resolve through each other, an abstract +// collection must refuse the subsetters that leave it short of its lower bound, the +// objects `all T` enumerates must not depend on the order usages were read, a binding +// must carry into an adopted context, and a probe must unwind one on rollback. +func TestRuntimeRobustnessExtentModelDetermined(t *testing.T) { + t.Run("a namespace binding refusing conflicting ends leaves nothing behind", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car = new Car(); + part b : Car = new Car(); + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + _, err := evalIn(t, ctx, pkg.Scope, "a") + if !errors.Is(err, ErrBindingConflict) { + t.Fatalf("a = %v, want binding conflict", err) + } + for _, name := range []string{"test::a", "test::b"} { + sym := lookupOne(t, idx, name) + if ids := ctx.occurrences[sym]; len(ids) != 0 { + t.Fatalf("occurrences[%s] = %v, want none: the refused binding records no occurrences", name, ids) + } + } + if len(ctx.instances) != 2 { + t.Fatalf("%d objects materialized, want the two stated values only", len(ctx.instances)) + } + }) + + t.Run("a namespace binding cycle is refused", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car = b; + part b : Car = a; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + _, err := evalIn(t, ctx, pkg.Scope, "a") + if !errors.Is(err, ErrCyclicFeatureValue) { + t.Fatalf("a = %v, want cyclic feature value dependency", err) + } + }) + + t.Run("a class of two differing valued members leaves nothing bound", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car = new Car(); + part b : Car = new Car(); + part c : Car; + bind a = b; + bind b = c; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + _, err := evalIn(t, ctx, pkg.Scope, "c") + if !errors.Is(err, ErrBindingConflict) { + t.Fatalf("c = %v, want binding conflict", err) + } + sym := lookupOne(t, idx, "test::c") + if ids := ctx.occurrences[sym]; len(ids) != 0 { + t.Fatalf("occurrences[test::c] = %v, want none: the refused class records no occurrences", ids) + } + if _, ok := ctx.namespaceBindings[sym]; ok { + t.Fatal("c is bound, want nothing bound after the refused class") + } + }) + + t.Run("the class's value is the same whichever member is read first", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car; + part b : Car; + bind a = b; + part e : Car; + bind b = e; + }` + extents := make([][]int64, 2) + for i, first := range []string{"e", "a"} { + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + if _, err := evalIn(t, ctx, pkg.Scope, first); err != nil { + t.Fatalf("%s: %v", first, err) + } + for _, expr := range []string{"a === e", "a === b"} { + same, err := evalIn(t, ctx, pkg.Scope, expr) + if err != nil || FormatValue(same) != "true" { + t.Fatalf("%s after %s = %v (%v), want true", expr, first, FormatValue(same), err) + } + } + all, err := evalIn(t, ctx, pkg.Scope, "all test::Car") + if err != nil { + t.Fatalf("all test::Car: %v", err) + } + extents[i] = heldObjects(all) + } + if !slices.Equal(extents[0], extents[1]) { + t.Fatalf("extent read from e first = %v, from a first = %v", extents[0], extents[1]) + } + if len(extents[0]) != 1 { + t.Fatalf("extent = %v, want the class's one object", extents[0]) + } + }) + + t.Run("an optional subsetter contributes its object once", func(t *testing.T) { + src := `package test { + part def Car; + part vs : Car[1]; + part opt : Car[0..1] :> vs; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + allCold, err := evalIn(t, ctx, pkg.Scope, "all test::Car") + if err != nil { + t.Fatalf("all test::Car: %v", err) + } + if ids := heldObjects(allCold); len(ids) != 1 { + t.Fatalf("cold all test::Car = %v, want the one object the optional subsetter fills", ids) + } + var firstIDs []int64 + for i := 0; i < 2; i++ { + got, err := evalIn(t, ctx, pkg.Scope, "vs") + if err != nil { + t.Fatalf("vs read %d: %v", i, err) + } + ids := heldObjects(got) + if len(ids) != 1 { + t.Fatalf("vs read %d = %v, want the one member the optional subsetter contributes", i, ids) + } + if i == 0 { + firstIDs = ids + } else if !slices.Equal(ids, firstIDs) { + t.Fatalf("vs read warm = %v, cold = %v, want the same object", ids, firstIDs) + } + } + allWarm, err := evalIn(t, ctx, pkg.Scope, "all test::Car") + if err != nil { + t.Fatalf("all test::Car warm: %v", err) + } + if !slices.Equal(heldObjects(allWarm), heldObjects(allCold)) { + t.Fatalf("warm extent = %v, cold = %v", heldObjects(allWarm), heldObjects(allCold)) + } + optVal, err := evalIn(t, ctx, pkg.Scope, "opt") + if err != nil { + t.Fatalf("opt: %v", err) + } + if ids := heldObjects(optVal); !slices.Equal(ids, firstIDs) { + t.Fatalf("opt = %v, want the object vs filled it with, %v", ids, firstIDs) + } + optDeclared, err := ctx.EvalDeclaredValue(lookupOne(t, idx, "test::opt")) + if err != nil { + t.Fatalf("EvalDeclaredValue opt: %v", err) + } + if ids := heldObjects(optDeclared); !slices.Equal(ids, firstIDs) { + t.Fatalf("EvalDeclaredValue opt = %v, want %v", ids, firstIDs) + } + }) + + t.Run("a valueless scalar binding leaves both members undetermined", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + attribute a : Integer; + attribute b : Integer; + bind a = b; + attribute c : Integer; + attribute d : Integer = 5; + bind c = d; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + for _, name := range []string{"a", "b"} { + got, err := evalIn(t, ctx, pkg.Scope, name) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + if FormatValue(got) != "" { + t.Fatalf("%s = %s, want undetermined: a valueless scalar binding fabricates no object", name, FormatValue(got)) + } + declared, err := ctx.EvalDeclaredValue(lookupOne(t, idx, "test::"+name)) + if err != nil { + t.Fatalf("EvalDeclaredValue %s: %v", name, err) + } + if FormatValue(declared) != "" { + t.Fatalf("EvalDeclaredValue %s = %s, want undetermined", name, FormatValue(declared)) + } + } + if len(ctx.namespaceBindings) != 0 { + t.Fatalf("namespace bindings = %v, want none: a valueless scalar class records nothing", ctx.namespaceBindings) + } + if len(ctx.instances) != 0 { + t.Fatalf("instances = %d, want none: no objects were materialized", len(ctx.instances)) + } + got, err := evalIn(t, ctx, pkg.Scope, "c") + if err != nil { + t.Fatalf("c: %v", err) + } + if FormatValue(got) != "5" { + t.Fatalf("c = %s, want 5: a valued scalar binding still carries the value", FormatValue(got)) + } + }) + + t.Run("a valueless structured binding shares one value", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + attribute def Point { attribute x : Integer = 1; } + attribute a : Point[1]; + attribute b : Point[1]; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + same, err := evalIn(t, ctx, pkg.Scope, "a === b") + if err != nil { + t.Fatalf("a === b: %v", err) + } + if FormatValue(same) != "true" { + t.Fatalf("a === b = %s, want true: a structured value's binding shares the one it denotes", FormatValue(same)) + } + got, err := evalIn(t, ctx, pkg.Scope, "a.x") + if err != nil { + t.Fatalf("a.x: %v", err) + } + if FormatValue(got) != "1" { + t.Fatalf("a.x = %s, want 1: the shared structured value carries its features", FormatValue(got)) + } + aVal, err := evalIn(t, ctx, pkg.Scope, "a") + if err != nil { + t.Fatalf("a: %v", err) + } + bVal, err := evalIn(t, ctx, pkg.Scope, "b") + if err != nil { + t.Fatalf("b: %v", err) + } + if ids := heldObjects(aVal); !slices.Equal(ids, heldObjects(bVal)) || len(ids) != 1 { + t.Fatalf("a = %v, b = %v, want the class's one Point", ids, heldObjects(bVal)) + } + }) + + t.Run("a class's materialized members honor every member's multiplicity", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car[2]; + part b : Car[2]; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + aVal, err := evalIn(t, ctx, pkg.Scope, "a") + if err != nil { + t.Fatalf("a: %v", err) + } + if ids := heldObjects(aVal); len(ids) != 2 { + t.Fatalf("a = %v, want its two lower-bound occurrences", ids) + } + bVal, err := evalIn(t, ctx, pkg.Scope, "b") + if err != nil { + t.Fatalf("b: %v", err) + } + if !slices.Equal(heldObjects(bVal), heldObjects(aVal)) { + t.Fatalf("b = %v, a = %v, want the same identities in the same order", heldObjects(bVal), heldObjects(aVal)) + } + all, err := evalIn(t, ctx, pkg.Scope, "all test::Car") + if err != nil { + t.Fatalf("all test::Car: %v", err) + } + if ids := heldObjects(all); len(ids) != 2 { + t.Fatalf("all test::Car = %v, want the class's two occurrences", ids) + } + }) + + t.Run("a class whose members' multiplicities disagree is refused and leaves nothing", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car[2]; + part b : Car[3]; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + _, err := evalIn(t, ctx, pkg.Scope, "a") + if !errors.Is(err, ErrBindingConflict) { + t.Fatalf("a = %v, want binding conflict: two occurrences cannot be b's [3]", err) + } + for _, name := range []string{"test::a", "test::b"} { + sym := lookupOne(t, idx, name) + if ids := ctx.occurrences[sym]; len(ids) != 0 { + t.Fatalf("occurrences[%s] = %v, want none: the refused class records no occurrences", name, ids) + } + if _, ok := ctx.namespaceBindings[sym]; ok { + t.Fatalf("%s is bound, want nothing bound after the refused class", name) + } + } + if len(ctx.instances) != 0 { + t.Fatalf("%d objects materialized, want none after the refused class", len(ctx.instances)) + } + }) + + t.Run("a member's existing occurrence joins its class's sources", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car; + part b : Car = new Car(); + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + aSym := lookupOne(t, idx, "test::a") + + inst, err := ctx.materialize(aSym, 0, nil, "") + if err != nil { + t.Fatalf("materialize a: %v", err) + } + ctx.occurrences[aSym] = []int64{inst.ID} + if _, err := evalIn(t, ctx, pkg.Scope, "a"); !errors.Is(err, ErrBindingConflict) { + t.Fatalf("a = %v, want binding conflict: a's recorded occurrence differs from b's value", err) + } + }) + + t.Run("a member's existing occurrence matching its class's value is no conflict", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car; + part b : Car = new Car(); + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + aSym := lookupOne(t, idx, "test::a") + bSym := lookupOne(t, idx, "test::b") + + ec := NewEvalContext(ctx, bSym.OwnerScope) + bVal, err := ec.declaredValue(bSym, bSym.Decl.(*ast.Usage).Value) + if err != nil { + t.Fatalf("b's declared value: %v", err) + } + ctx.occurrences[aSym] = heldObjects(bVal) + got, err := evalIn(t, ctx, pkg.Scope, "a") + if err != nil { + t.Fatalf("a = %v: a's recorded occurrence is b's value, so there is no conflict", err) + } + if !slices.Equal(heldObjects(got), heldObjects(bVal)) { + t.Fatalf("a = %v, want b's occurrence %v", heldObjects(got), heldObjects(bVal)) + } + same, err := evalIn(t, ctx, pkg.Scope, "a === b") + if err != nil || FormatValue(same) != "true" { + t.Fatalf("a === b = %v (%v), want true: one recorded object is the member's scalar value", FormatValue(same), err) + } + }) + + t.Run("a member's recorded occurrences match a multi-valued source", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car[2]; + part b : Car[2] = (new Car(), new Car()); + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + aSym := lookupOne(t, idx, "test::a") + bSym := lookupOne(t, idx, "test::b") + + ec := NewEvalContext(ctx, bSym.OwnerScope) + bVal, err := ec.declaredValue(bSym, bSym.Decl.(*ast.Usage).Value) + if err != nil { + t.Fatalf("b's declared value: %v", err) + } + if ids := heldObjects(bVal); len(ids) != 2 { + t.Fatalf("b's declared value = %v, want two occurrences", ids) + } + ctx.occurrences[aSym] = heldObjects(bVal) + got, err := evalIn(t, ctx, pkg.Scope, "a") + if err != nil { + t.Fatalf("a = %v: a's recorded occurrences are b's value, so there is no conflict", err) + } + if !slices.Equal(heldObjects(got), heldObjects(bVal)) { + t.Fatalf("a = %v, want b's occurrences %v", heldObjects(got), heldObjects(bVal)) + } + }) + + t.Run("a class materializes the largest member lower bound", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car[0..1]; + part b : Car; + bind a = b; + }` + extents := make([][]int64, 2) + for i, first := range []string{"a", "b"} { + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + got, err := evalIn(t, ctx, pkg.Scope, first) + if err != nil { + t.Fatalf("%s first: %v", first, err) + } + if ids := heldObjects(got); len(ids) != 1 { + t.Fatalf("%s first = %v, want the one occurrence the class materializes", first, ids) + } + all, err := evalIn(t, ctx, pkg.Scope, "all test::Car") + if err != nil { + t.Fatalf("all test::Car after %s first: %v", first, err) + } + extents[i] = heldObjects(all) + } + if !slices.Equal(extents[0], extents[1]) || len(extents[0]) != 1 { + t.Fatalf("extent read from a first = %v, from b first = %v, want the same one object", extents[0], extents[1]) + } + }) + + t.Run("a class whose largest lower bound a member cannot hold is refused", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car[0..1]; + part b : Car[2]; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + _, err := evalIn(t, ctx, pkg.Scope, "b") + if !errors.Is(err, ErrBindingConflict) { + t.Fatalf("b = %v, want binding conflict: two occurrences cannot be a's [0..1]", err) + } + for _, name := range []string{"test::a", "test::b"} { + sym := lookupOne(t, idx, name) + if ids := ctx.occurrences[sym]; len(ids) != 0 { + t.Fatalf("occurrences[%s] = %v, want none: the refused class records no occurrences", name, ids) + } + if _, ok := ctx.namespaceBindings[sym]; ok { + t.Fatalf("%s is bound, want nothing bound after the refused class", name) + } + } + if len(ctx.instances) != 0 { + t.Fatalf("%d objects materialized, want none after the refused class", len(ctx.instances)) + } + }) + + t.Run("an optional attribute bound to a value reads that value", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + attribute a : Integer[0..1]; + attribute b : Integer = 5; + bind a = b; + attribute u : Integer[0..1]; + }` + for _, first := range []string{"a", "b"} { + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + if _, err := evalIn(t, ctx, pkg.Scope, first); err != nil { + t.Fatalf("%s first: %v", first, err) + } + got, err := evalIn(t, ctx, pkg.Scope, "a") + if err != nil { + t.Fatalf("a after %s first: %v", first, err) + } + if FormatValue(got) != "5" { + t.Fatalf("a after %s first = %s, want 5: the binding's value, not an empty read", first, FormatValue(got)) + } + declared, err := ctx.EvalDeclaredValue(lookupOne(t, idx, "test::a")) + if err != nil { + t.Fatalf("EvalDeclaredValue a after %s first: %v", first, err) + } + if FormatValue(declared) != "5" { + t.Fatalf("EvalDeclaredValue a after %s first = %s, want 5, as the expression read gives", first, FormatValue(declared)) + } + } + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + got, err := evalIn(t, ctx, pkg.Scope, "u") + if err != nil { + t.Fatalf("u: %v", err) + } + if FormatValue(got) == "5" { + t.Fatalf("u = %s, want undetermined: an unbound optional attribute still reads as before", FormatValue(got)) + } + undeclared, err := ctx.EvalDeclaredValue(lookupOne(t, idx, "test::u")) + if err != nil { + t.Fatalf("EvalDeclaredValue u: %v", err) + } + if FormatValue(undeclared) != "" { + t.Fatalf("EvalDeclaredValue u = %s, want undetermined, as before", FormatValue(undeclared)) + } + }) + + t.Run("a required attribute bound to a value reads that value", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + attribute a : Integer; + attribute b : Integer = 5; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + got, err := evalIn(t, ctx, pkg.Scope, "a") + if err != nil { + t.Fatalf("a: %v", err) + } + if FormatValue(got) != "5" { + t.Fatalf("a = %s, want 5: a valueless usage a binding governs reads the binding's value", FormatValue(got)) + } + declared, err := ctx.EvalDeclaredValue(lookupOne(t, idx, "test::a")) + if err != nil { + t.Fatalf("EvalDeclaredValue a: %v", err) + } + if FormatValue(declared) != "5" { + t.Fatalf("EvalDeclaredValue a = %s, want 5, as the expression read gives", FormatValue(declared)) + } + }) + + t.Run("an optional part bound to a valued part reads its object", func(t *testing.T) { + src := `package test { + part def Car; + part p : Car[0..1]; + part q : Car = new Car(); + bind p = q; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + same, err := evalIn(t, ctx, pkg.Scope, "p === q") + if err != nil || FormatValue(same) != "true" { + t.Fatalf("p === q = %v (%v), want true", FormatValue(same), err) + } + }) + + t.Run("a member's classifier behavior reads the class's one object", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + part def Car { + attribute probe : Car; + exhibit state tally { + entry; then on; + state on { entry action peek { assign probe := a; } } + } + } + part a : Car; + part b : Car; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + if _, err := evalIn(t, ctx, pkg.Scope, "a"); err != nil { + t.Fatalf("a: %v", err) + } + all, err := evalIn(t, ctx, pkg.Scope, "all test::Car") + if err != nil { + t.Fatalf("all test::Car: %v", err) + } + if ids := heldObjects(all); len(ids) != 1 { + t.Fatalf("all test::Car = %v, want the class's one object: a behavior reading a member must not materialize another", ids) + } + }) + + t.Run("a scope registered after the index builds still resolves its bindings", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car; + part b : Car; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + if _, err := evalIn(t, ctx, pkg.Scope, "a"); err != nil { + t.Fatalf("a: %v", err) + } + + file := parser.New(source.New("", []byte(`package other { + part def Car; + part c : Car; + part d : Car; + bind c = d; + }`))).ParseFile() + idx.AddDocument("", file) + scope := idx.DocumentRoot("") + ctx.Model().RegisterScope(scope) + + same, err := evalIn(t, ctx, scope, "other::c === other::d") + if err != nil { + t.Fatalf("other::c === other::d: %v", err) + } + if FormatValue(same) != "true" { + t.Fatalf("other::c === other::d = %s, want true: the registered tree's binding must resolve", FormatValue(same)) + } + }) + + t.Run("an abstract collection under-count is refused", func(t *testing.T) { + src := `package test { + part def Car; + abstract part vs : Car[2]; + part c1 : Car[1] :> vs; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + _, err := evalIn(t, ctx, pkg.Scope, "vs") + if !errors.Is(err, ErrMultiplicityViolation) { + t.Fatalf("vs = %v, want multiplicity violation", err) + } + if !strings.Contains(err.Error(), "vs") { + t.Fatalf("vs = %v, want the violation naming vs", err) + } + }) + + t.Run("the extent is the same cold and after the members were read", func(t *testing.T) { + src := `package test { + part def Car; + abstract part vs : Car[1..*]; + part c1 : Car[1] :> vs; + part c2 : Car[1] :> vs; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + cold, err := evalIn(t, ctx, pkg.Scope, "all test::Car") + if err != nil { + t.Fatalf("all test::Car: %v", err) + } + coldIDs := heldObjects(cold) + for _, expr := range []string{"c1", "c2", "vs"} { + if _, err := evalIn(t, ctx, pkg.Scope, expr); err != nil { + t.Fatalf("%s: %v", expr, err) + } + } + warm, err := evalIn(t, ctx, pkg.Scope, "all test::Car") + if err != nil { + t.Fatalf("all test::Car: %v", err) + } + warmIDs := heldObjects(warm) + if !slices.Equal(warmIDs, coldIDs) { + t.Fatalf("extent after the reads = %v, cold extent = %v", warmIDs, coldIDs) + } + }) + + t.Run("adoption keeps a namespace binding", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car; + part b : Car; + bind a = b; + }` + prev, _ := contextForSource(t, src) + prevPkg := lookupOne(t, prev.model.resolver.Index(), "test") + got, err := evalIn(t, prev, prevPkg.Scope, "a") + if err != nil { + t.Fatalf("a: %v", err) + } + obj := prev.instances[heldObjects(got)[0]] + + ctx := contextOver(t, src) + pkg := lookupOne(t, ctx.model.resolver.Index(), "test") + if _, err := ctx.Adopt(prev, prev.ShapesOf(obj), obj); err != nil { + t.Fatalf("Adopt: %v", err) + } + carried, err := evalIn(t, ctx, pkg.Scope, "a") + if err != nil { + t.Fatalf("a after adoption: %v", err) + } + if heldObjects(carried)[0] != obj.ID { + t.Fatalf("a after adoption = object %d, want carried object %d", heldObjects(carried)[0], obj.ID) + } + same, err := evalIn(t, ctx, pkg.Scope, "a === b") + if err != nil { + t.Fatalf("a === b: %v", err) + } + if FormatValue(same) != "true" { + t.Fatalf("a === b after adoption = %s, want true", FormatValue(same)) + } + }) + + t.Run("a reader holding another tree's symbols for a class reads its shared value", func(t *testing.T) { + src := `package test { + part def Car; + part x : Car; + part y : Car = new Car(); + bind x = y; + }` + file := parser.New(source.New("", []byte(src))).ParseFile() + idx := symbols.NewIndex() + idx.AddDocument("", file) + resolver := resolve.New(idx) + ctx := NewContext(typedModel(semantics.NewModel(resolver), resolver), 10000) + pkg := lookupOne(t, idx, "test") + + same, err := evalIn(t, ctx, pkg.Scope, "x === y") + if err != nil { + t.Fatalf("x === y: %v", err) + } + if FormatValue(same) != "true" { + t.Fatalf("x === y = %s, want true", FormatValue(same)) + } + declared, err := ctx.EvalDeclaredValue(lookupOne(t, idx, "test::y")) + if err != nil { + t.Fatalf("EvalDeclaredValue y: %v", err) + } + if ids := heldObjects(declared); len(ids) != 1 { + t.Fatalf("EvalDeclaredValue y = %v, want y's one object", ids) + } + + // A session can hold a second scope tree over the same declarations — + // the index's tree and the prompt's differ — whose symbols reach the + // reads anyway; the class answers through the declaration they name. + other := symbols.NewIndex() + other.AddDocument("", file) + otherScope := other.DocumentRoot("") + otherSame, err := evalIn(t, ctx, otherScope, "test::x === test::y") + if err != nil { + t.Fatalf("other tree x === y: %v", err) + } + if FormatValue(otherSame) != "true" { + t.Fatalf("other tree x === y = %s, want true: the class resolves by declaration", FormatValue(otherSame)) + } + otherX := lookupOne(t, other, "test::x") + if otherX == lookupOne(t, idx, "test::x") { + t.Fatalf("the other tree's x is the index's symbol; the test needs a different one") + } + otherDeclared, err := ctx.EvalDeclaredValue(otherX) + if err != nil { + t.Fatalf("EvalDeclaredValue other-tree x: %v", err) + } + if !slices.Equal(heldObjects(otherDeclared), heldObjects(declared)) { + t.Fatalf("other-tree x = %v, want the shared object %v", heldObjects(otherDeclared), heldObjects(declared)) + } + }) + + t.Run("a probe undo forgets a namespace binding", func(t *testing.T) { + src := `package test { + part def Car; + part a : Car; + part b : Car; + bind a = b; + }` + ctx, idx := contextForSource(t, src) + pkg := lookupOne(t, idx, "test") + + end := ctx.beginProbe() + if _, err := evalIn(t, ctx, pkg.Scope, "a"); err != nil { + t.Fatalf("a: %v", err) + } + end() + if len(ctx.namespaceBindings) != 0 { + t.Fatalf("namespace bindings after undo = %v, want none", ctx.namespaceBindings) + } + same, err := evalIn(t, ctx, pkg.Scope, "a === b") + if err != nil { + t.Fatalf("a === b: %v", err) + } + if FormatValue(same) != "true" { + t.Fatalf("a === b after undo = %s, want true", FormatValue(same)) + } + }) + + t.Run("a bound collection class holds its lower bound lazily", func(t *testing.T) { + model, resolver, root := parseAndBuildLibraryModel(t, `package test { + private import BaseFunctions::*; + private import SequenceFunctions::*; + part def Car; + part a : Car[1000000000]; + part b : Car[1000000000]; + bind a = b; + }`) + ctx := NewContext(typedModel(model, resolver), DefaultMaxSteps) + pkg := resolveSymbol(t, root, "test") + + for _, src := range []string{"size(a)", "size(b)"} { + got, err := evalIn(t, ctx, pkg.Scope, src) + if err != nil || FormatValue(got) != "1000000000" { + t.Fatalf("%s = %s, %v; want 1000000000", src, FormatValue(got), err) + } + } + for _, src := range []string{"a#(5) === b#(5)", "a#(999999999) === b#(999999999)"} { + got, err := evalIn(t, ctx, pkg.Scope, src) + if err != nil || FormatValue(got) != "true" { + t.Fatalf("%s = %s, %v; want true", src, FormatValue(got), err) + } + } + if n := len(ctx.instances); n >= 1100 { + t.Fatalf("a billion bound values made %d objects", n) + } + }) + + t.Run("a subsetting member of a lazy namespace collection is among its values", func(t *testing.T) { + model, resolver, root := parseAndBuildLibraryModel(t, `package test { + private import BaseFunctions::*; + private import SequenceFunctions::*; + part def Car; + part vs : Car[1000000000]; + part c : Car :> vs; + }`) + ctx := NewContext(typedModel(model, resolver), DefaultMaxSteps) + pkg := resolveSymbol(t, root, "test") + + got, err := evalIn(t, ctx, pkg.Scope, "size(vs)") + if err != nil || FormatValue(got) != "1000000000" { + t.Fatalf("size(vs) = %s, %v; want 1000000000", FormatValue(got), err) + } + same, err := evalIn(t, ctx, pkg.Scope, "vs#(1) === c") + if err != nil || FormatValue(same) != "true" { + t.Fatalf("vs#(1) === c = %s, %v; want the subsetter first", FormatValue(same), err) + } + again, err := evalIn(t, ctx, pkg.Scope, "size(vs)") + if err != nil || FormatValue(again) != "1000000000" { + t.Fatalf("size(vs) read again = %s, %v; want 1000000000", FormatValue(again), err) + } + if n := len(ctx.instances); n >= 1100 { + t.Fatalf("a billion subsetted values made %d objects", n) + } + }) + + t.Run("an open-ended optional subsetter of a lazy collection is refused whole", func(t *testing.T) { + model, resolver, root := parseAndBuildLibraryModel(t, `package test { + private import SequenceFunctions::*; + part def Car; + part vs : Car[1000000000]; + part o : Car[0..*] :> vs; + }`) + ctx := NewContext(typedModel(model, resolver), DefaultMaxSteps) + pkg := resolveSymbol(t, root, "test") + + _, err := evalIn(t, ctx, pkg.Scope, "size(vs)") + if !errors.Is(err, ErrElementLimitExceeded) { + t.Fatalf("size(vs) = %v, want %v", err, ErrElementLimitExceeded) + } + if n := len(ctx.instances); n >= 1100 { + t.Fatalf("the refused fill made %d objects", n) + } + }) + + t.Run("a bound class of differing collections stays eager", func(t *testing.T) { + model, resolver, root := parseAndBuildLibraryModel(t, `package test { + private import BaseFunctions::*; + part def A; + part def B; + part a : A[2000]; + part b : B[2000]; + bind a = b; + }`) + ctx := NewContext(typedModel(model, resolver), DefaultMaxSteps) + pkg := resolveSymbol(t, root, "test") + + got, err := evalIn(t, ctx, pkg.Scope, "a#(1500) istype test::B") + if err != nil || FormatValue(got) != "true" { + t.Fatalf("a#(1500) istype B = %s, %v; want true", FormatValue(got), err) + } + }) + + t.Run("the extent of a bound collection is refused like an unbound one", func(t *testing.T) { + for _, bound := range []bool{false, true} { + src := `package test { + private import SequenceFunctions::*; + part def Car; + part a : Car[1000000000]; + }` + if bound { + src = `package test { + private import SequenceFunctions::*; + part def Car; + part a : Car[1000000000]; + part b : Car[1000000000]; + bind a = b; + }` + } + model, resolver, root := parseAndBuildLibraryModel(t, src) + ctx := NewContext(typedModel(model, resolver), DefaultMaxSteps) + pkg := resolveSymbol(t, root, "test") + if _, err := evalIn(t, ctx, pkg.Scope, "size(a)"); err != nil { + t.Fatalf("bound=%v size(a): %v", bound, err) + } + _, err := evalIn(t, ctx, pkg.Scope, "size(all test::Car)") + if !errors.Is(err, ErrElementLimitExceeded) { + t.Fatalf("bound=%v size(all Car) = %v, want %v", bound, err, ErrElementLimitExceeded) + } + } + }) + + t.Run("a member declaring its own features keeps the class eager", func(t *testing.T) { + model, resolver, root := parseAndBuildLibraryModel(t, `package test { + private import ScalarValues::*; + private import BaseFunctions::*; + part def Car; + part a : Car[2000]; + part b : Car[2000] { attribute label : String = "ready"; } + bind a = b; + }`) + ctx := NewContext(typedModel(model, resolver), DefaultMaxSteps) + pkg := resolveSymbol(t, root, "test") + + for _, src := range []string{`b#(1).label == "ready"`, `a#(1).label == "ready"`} { + got, err := evalIn(t, ctx, pkg.Scope, src) + if err != nil || FormatValue(got) != "true" { + t.Fatalf("%s = %s, %v; want true", src, FormatValue(got), err) + } + } + }) + + t.Run("a tailed source value shares its population with every member", func(t *testing.T) { + model, resolver, root := parseAndBuildLibraryModel(t, `package test { + private import ScalarValues::*; + private import SequenceFunctions::*; + private import BaseFunctions::*; + part def Car; + part c : Car[2000]; + part a : Car[2000] = c; + part b : Car[2000]; + bind a = b; + }`) + ctx := NewContext(typedModel(model, resolver), DefaultMaxSteps) + pkg := resolveSymbol(t, root, "test") + + first, err := evalIn(t, ctx, pkg.Scope, "c#(1)") + if err != nil { + t.Fatalf("c#(1): %v", err) + } + for _, src := range []string{"size(a)", "size(b)"} { + v, err := evalIn(t, ctx, pkg.Scope, src) + if err != nil || FormatValue(v) != "2000" { + t.Fatalf("%s = %s, %v; want 2000", src, FormatValue(v), err) + } + } + for _, src := range []string{"b#(1) === c#(1)", "a#(1) === c#(1)"} { + v, err := evalIn(t, ctx, pkg.Scope, src) + if err != nil || FormatValue(v) != "true" { + t.Fatalf("%s = %s, %v; want true", src, FormatValue(v), err) + } + } + bSym := resolveSymbol(t, pkg.Scope, "b") + objs, err := ctx.denotedObjects(bSym) + if err != nil { + t.Fatalf("denotedObjects(b): %v", err) + } + if len(objs) != 2000 { + t.Fatalf("denotedObjects(b) = %d objects, want 2000", len(objs)) + } + if !containsInstance(objs, first.Instance) { + t.Fatalf("denotedObjects(b) misses object #%d (c#(1))", first.Instance) + } + }) +} diff --git a/internal/exec/runtime/robustness_object_lifecycle_test.go b/internal/exec/runtime/robustness_object_lifecycle_test.go index 8d3ec3fa10..cdccd39af0 100644 --- a/internal/exec/runtime/robustness_object_lifecycle_test.go +++ b/internal/exec/runtime/robustness_object_lifecycle_test.go @@ -1216,7 +1216,7 @@ const objectLifecycleExploreModel = ` join sync; action read { assign count := size(all Car); - assign distinct := not (left === right); + then assign distinct := not (left === right); } done; succession first start then split; diff --git a/internal/exec/runtime/robustness_resumable_inline_do_body_test.go b/internal/exec/runtime/robustness_resumable_inline_do_body_test.go index 47f06d2b18..93efa48452 100644 --- a/internal/exec/runtime/robustness_resumable_inline_do_body_test.go +++ b/internal/exec/runtime/robustness_resumable_inline_do_body_test.go @@ -13,6 +13,7 @@ func TestRuntimeRobustnessResumableInlineDoBody(t *testing.T) { t.Run("exit_mid_loop_drops_the_pending_iterations", testDoBodyExitMidLoopDropsThePendingIterations) t.Run("exit_mid_iteration_drops_the_rest_of_the_iteration", testDoBodyExitMidIterationDropsTheRestOfTheIteration) t.Run("empty_branch_is_a_round_of_its_own", testDoBodyEmptyBranchIsARoundOfItsOwn) + t.Run("stated_flow_loop_and_if_nodes_yield_as_statements", testDoBodyStatedFlowNodesYieldAsStatements) t.Run("exit_on_a_clock_wait_after_a_loop_leaves_no_timer", testDoBodyExitOnAClockWaitAfterALoopLeavesNoTimer) t.Run("non_terminating_body_exceeds_the_step_limit", testDoBodyNonTerminatingExceedsTheStepLimit) t.Run("non_terminating_flow_body_exceeds_the_step_limit", testDoBodyNonTerminatingFlowExceedsTheStepLimit) @@ -169,6 +170,44 @@ func testDoBodyEmptyBranchIsARoundOfItsOwn(t *testing.T) { } } +// testDoBodyStatedFlowNodesYieldAsStatements: a `for` or `if` node of a do body's +// stated flow yields after each iteration and branch statement, as in a statement list: +// the round before the Stop runs one more, and the rest is dropped. +func testDoBodyStatedFlowNodesYieldAsStatements(t *testing.T) { + for _, c := range []struct { + body string + rounds []int64 + after int64 + }{ + {"assign total := 0; then for i in 1..5 { assign total := total + i; }", []int64{0, 1, 3}, 6}, + {"assign total := 1; then if true { assign total := total + 1; then assign total := total * 10; then assign total := total + 5; }", []int64{1, 2}, 20}, + {"assign total := 1; then while total < 1000 { assign total := total * 10; }", []int64{1, 10}, 100}, + } { + goroutines := goruntime.NumGoroutine() + exec := stateExecutorForSource(t, "Machine", doBodyMachine(c.body)) + run := pausedDoRun(t, exec) + for round, want := range c.rounds { + if round > 0 { + if _, err := exec.RunDoRound(); err != nil { + t.Fatalf("%s: do round %d: %v", c.body, round+1, err) + } + } + if total := exec.StateData()["total"]; !valueEqual(total, integerValue(want)) { + t.Fatalf("%s: total = %v after %d round(s); want %d", c.body, total, round+1, want) + } + } + exec.SendSignal("Stop", nil) + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("%s: run to completion: %v", c.body, err) + } + data := exec.StateData() + if !valueEqual(data["total"], integerValue(c.after)) || !valueEqual(data["after"], integerValue(c.after)) { + t.Errorf("%s: total = %v, after = %v; want %d and %d: one round before the Stop, the rest dropped", c.body, data["total"], data["after"], c.after, c.after) + } + assertDoBodyAbandoned(t, exec, run, goroutines) + } +} + // testDoBodyExitOnAClockWaitAfterALoopLeavesNoTimer: a body whose flow loops in // one node, then waits on the clock at the next — the one round performs the node // and parks the token at the wait — is exited by the Stop while the wait is armed; diff --git a/internal/exec/runtime/robustness_terminate_body_flow_test.go b/internal/exec/runtime/robustness_terminate_body_flow_test.go new file mode 100644 index 0000000000..ba04bd95d9 --- /dev/null +++ b/internal/exec/runtime/robustness_terminate_body_flow_test.go @@ -0,0 +1,112 @@ +package runtime + +import ( + "errors" + "slices" + "testing" +) + +// TestRuntimeRobustnessTerminateBodyFlow exercises a terminate action usage whose +// body states a flow: the flow runs, a `terminate` in its chain ends it early, and +// a flow the successions leave no start to is a typed error at initialize. +func TestRuntimeRobustnessTerminateBodyFlow(t *testing.T) { + t.Run("stated_flow_runs_and_ends_the_performance", testTerminateBodyFlowRuns) + t.Run("then_chain_reaches_the_terminate_early", testTerminateBodyFlowEndsEarly) + t.Run("succession_cycle_leaves_no_start", testTerminateBodyFlowCycle) + t.Run("succession_to_an_unknown_step", testTerminateBodyFlowUnknownStep) +} + +func testTerminateBodyFlowRuns(t *testing.T) { + outputs, err := executeActionSource(t, "host", `package test { + private import ScalarValues::*; + action host { + 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; + } + }`) + if err != nil { + t.Fatalf("execute: %v", err) + } + assertIntOutput(t, outputs, "x", 1) + assertIntOutput(t, outputs, "later", 0) +} + +func testTerminateBodyFlowEndsEarly(t *testing.T) { + m := parseExploreModel(t, `package test { + private import ScalarValues::*; + action early { + out attribute x : Integer = 0; + out attribute y : Integer = 0; + first start; + then stop; + action stop terminate { + assign x := 1; + then terminate; + then assign y := 5; + } + then done; + } + }`) + x := m.exploreAction(t, "explore", "early") + if !x.Complete() { + t.Fatalf("exploration %s, want complete", x.Status()) + } + xs := featureValues(t, x, "x") + slices.Sort(xs) + if xs = slices.Compact(xs); !slices.Equal(xs, []string{"0", "1"}) { + t.Errorf("x over every schedule = %v, want [0 1]: the implicit terminate before or after the assign", xs) + } + if ys := slices.Compact(featureValues(t, x, "y")); !slices.Equal(ys, []string{"0"}) { + t.Errorf("y over every schedule = %v, want [0]: no statement after the terminate is performed", ys) + } +} + +func testTerminateBodyFlowCycle(t *testing.T) { + _, err := executeActionSource(t, "cycle", `package test { + private import ScalarValues::*; + action cycle { + out attribute x : Integer = 0; + first start; + then stop; + action stop terminate { + action a { assign x := 1; } + action b { assign x := 2; } + succession a then b; + succession b then a; + } + then done; + } + }`) + if !errors.Is(err, ErrInvalidActionFlow) { + t.Fatalf("error = %v, want ErrInvalidActionFlow", err) + } +} + +func testTerminateBodyFlowUnknownStep(t *testing.T) { + _, err := executeActionSource(t, "missing", `package test { + private import ScalarValues::*; + action missing { + out attribute x : Integer = 0; + first start; + then stop; + action stop terminate { + first start; + then nowhere; + then done; + } + then done; + } + }`) + if !errors.Is(err, ErrInvalidActionFlow) { + t.Fatalf("error = %v, want ErrInvalidActionFlow", err) + } +} diff --git a/internal/exec/runtime/robustness_terminate_test.go b/internal/exec/runtime/robustness_terminate_test.go index b9e69d50aa..21fba30466 100644 --- a/internal/exec/runtime/robustness_terminate_test.go +++ b/internal/exec/runtime/robustness_terminate_test.go @@ -20,7 +20,6 @@ func TestRuntimeRobustnessTerminate(t *testing.T) { t.Run("terminate_of_a_part_reached_after_its_whole_ended", testTerminateOfAPartReachedAfterItsWholeEnded) t.Run("terminate_of_an_occurrence_expression", testTerminateOfAnOccurrenceExpression) t.Run("terminate_of_a_node_of_a_sibling_flow", testTerminateOfANodeOfASiblingFlow) - t.Run("terminate_usage_stating_a_flow_of_its_own", testTerminateUsageStatingAFlowOfItsOwn) t.Run("terminate_of_an_unknown_name_in_a_state_body", testTerminateOfAnUnknownNameInAStateBody) t.Run("terminate_of_an_ended_occurrence_in_a_state_body", testTerminateOfAnEndedOccurrenceInAStateBody) t.Run("terminate_of_a_value_in_a_transition_effect", testTerminateOfAValueInATransitionEffect) @@ -60,28 +59,6 @@ func testTerminateOfAnUnknownName(t *testing.T) { } } -// testTerminateUsageStatingAFlowOfItsOwn: a terminate action usage whose body states -// a flow of its own is refused at initialize, not run with the flow dropped. -func testTerminateUsageStatingAFlowOfItsOwn(t *testing.T) { - _, err := executeActionSource(t, "host", `package test { - private import ScalarValues::*; - action host { - out attribute x : Integer = 0; - first start; - then stop; - action stop terminate { - first start; - then action inner { assign x := 1; } - then done; - } - then done; - } - }`) - if !errors.Is(err, ErrInvalidActionFlow) { - t.Fatalf("error = %v, want ErrInvalidActionFlow", err) - } -} - // testTerminateOfANonActionFeature: a terminate naming a feature that is no action // node names an occurrence, which an action body does not end yet. func testTerminateOfANonActionFeature(t *testing.T) { diff --git a/internal/exec/runtime/schedule_replay.go b/internal/exec/runtime/schedule_replay.go index ee59b349fb..5fca5d4023 100644 --- a/internal/exec/runtime/schedule_replay.go +++ b/internal/exec/runtime/schedule_replay.go @@ -41,7 +41,8 @@ func replaySchedule( if err := stop.Err(); err != nil { return nil, err } - choices := w.Choices + runWitness, finalOrders := splitFinalOrderChoices(w) + choices := runWitness.Choices if at != ScheduleEnd && (at < 0 || at > len(choices)) { return nil, &ReplayDisagreement{ Reason: fmt.Sprintf("the witness names move %d of a schedule of %d moves", at, len(choices)), @@ -52,13 +53,13 @@ func replaySchedule( if err != nil { return nil, err } - if err := ctx.SetSchedule(ReplayOf(Witness{Objects: w.Objects, Inputs: w.Inputs, DrawPolicy: w.DrawPolicy, ClockStep: w.ClockStep, Draws: w.Draws, Choices: choices})); err != nil { + if err := ctx.SetSchedule(ReplayOf(runWitness)); err != nil { return nil, err } if ctx.Trace() == nil { ctx.SetTrace(NewTraceRecorder()) } - r := &Replayed{Ctx: ctx} + r := &Replayed{Ctx: ctx, finalOrders: finalOrders} run, err := beginInvocation(ctx, start) if err != nil { r.Err = err @@ -135,7 +136,18 @@ func (r *Replayed) Evaluate(p CheckProperty) (bool, error) { // Outcome spells the run's outcome as a check reports a final state's. func (r *Replayed) Outcome() string { defer r.Ctx.beginProbe()() - return r.Inv.Outcome().String() + if len(r.finalOrders) == 0 { + return r.Inv.Outcome().String() + } + c := &checker{ctx: r.Ctx, inv: r.Inv} + var spelled string + if err := r.withFinalStatementOrders(func() error { + _, spelled, _, _ = c.spellFinalOnce() + return nil + }); err != nil { + return "error: " + err.Error() + } + return spelled } // FinalValue spells the feature's value as the run left it, UnsetText for one holding none: @@ -152,7 +164,13 @@ func (r *Replayed) FinalValue(feature string) (string, error) { if err := c.divergeReached(); err != nil { return "", err } - values, _, _, _ := c.spellFinal() + var values map[string]string + if err := r.withFinalStatementOrders(func() error { + values, _, _, _ = c.spellFinal() + return nil + }); err != nil { + return "", err + } value, held := values[c.divergenceKey(feature)] if !held { return "", &UnknownCheckFeatureError{Name: feature, Reason: "the run left no value under it"} diff --git a/internal/exec/runtime/scheduler.go b/internal/exec/runtime/scheduler.go index 087cccabf8..56021ce038 100644 --- a/internal/exec/runtime/scheduler.go +++ b/internal/exec/runtime/scheduler.go @@ -365,6 +365,33 @@ func (s *scheduler) oneMove() bool { return (s.policy.kind == scheduleExplore && s.explore != nil) || s.replay != nil || s.checking() } +// bodyYields reports how a token's body run pauses between its subperformances: +// at every boundary where a step is one move, at drawn ones under a seed while +// other tokens are live, at none under a fixed order. +func (s *scheduler) bodyYields(contended bool) (yields, draws bool) { + if s.oneMove() { + return true, false + } + seeded := s.policy.kind == scheduleSeeded && contended + return seeded, seeded +} + +// ordersStatements reports whether the run picks among the statements of a body +// that may run next: as it picks a step's move, or by its seed; a fixed order keeps +// declaration order. +func (s *scheduler) ordersStatements() bool { + return s.oneMove() || s.policy.kind == scheduleSeeded +} + +// drawYield draws whether a seeded body run for token yields at its boundary-th +// boundary: a function of the seed and both, so the run's other draws keep their stream. +func (s *scheduler) drawYield(token int64, boundary uint64) bool { + z := s.policy.seed ^ uint64(token)*0x9e3779b97f4a7c15 ^ boundary*0xbf58476d1ce4e5b9 // #nosec G115 -- the bits are hashed, not used as a count + z = (z ^ z>>30) * 0xbf58476d1ce4e5b9 + z = (z ^ z>>27) * 0x94d049bb133111eb + return (z^z>>31)&1 == 0 +} + // checking reports whether the run makes the moves the model checker selects. func (s *scheduler) checking() bool { return s.policy.kind == scheduleCheck && s.check != nil diff --git a/internal/exec/runtime/scheduler_test.go b/internal/exec/runtime/scheduler_test.go index 152e4867f4..dd83fbe009 100644 --- a/internal/exec/runtime/scheduler_test.go +++ b/internal/exec/runtime/scheduler_test.go @@ -66,9 +66,9 @@ const choiceModel = `package test { attribute order : String = ""; first start; fork split; - action a { assign x := 1; assign order := order + "a"; } - action b { assign x := 2; assign order := order + "b"; } - action c { assign x := 3; assign order := order + "c"; } + action a assign x := 1; then assign order := order + "a"; + action b assign x := 2; then assign order := order + "b"; + action c assign x := 3; then assign order := order + "c"; join sync; then decide select; if level > 50 then warn; @@ -159,10 +159,14 @@ func TestSeededSchedulingIsReproducible(t *testing.T) { if first != second { t.Fatalf("%s: two runs differ\n=== FIRST ===\n%s\n=== SECOND ===\n%s", policy, first, second) } - if len(choices) != 4 { - t.Fatalf("%s: choices = %v, want token order, two write orders and a decision branch", policy, choices) + kinds := make(map[ChoiceKind]int) + for _, c := range choices { + kinds[c.Kind]++ } - assertChoicesMatchRun(t, choices, outputs) + if kinds[ChoiceTokenOrder] < 1 || kinds[ChoiceWriteOrder] < 2 || kinds[ChoiceDecisionBranch] != 1 { + t.Fatalf("%s: choices = %v, want token orders, two or more write orders and a decision branch", policy, choices) + } + assertChoicesMatchTrace(t, policy, first, choices, outputs) traces[first] = true } if len(traces) < 3 { @@ -197,6 +201,65 @@ func assertChoicesMatchRun(t *testing.T, choices []ChoicePoint, outputs map[stri } } +// assertChoicesMatchTrace checks each choice point against the step of the trace +// it ends, as a run whose branch bodies a seed splits across steps writes it: the +// token stepped first wrote first, the write that stood was the step's last. +func assertChoicesMatchTrace(t *testing.T, policy SchedulePolicy, trace string, choices []ChoicePoint, outputs map[string]Value) { + t.Helper() + noted := make(map[string]ChoicePoint, len(choices)) + for _, c := range choices { + noted[c.String()] = c + } + type write struct{ feature, value, branch string } + var step []write + var target string + for _, line := range strings.Split(trace, "\n") { + switch { + case strings.HasPrefix(line, "step "): + step, target = nil, "" + case strings.HasPrefix(line, "stmt assign "): + target = strings.TrimPrefix(line, "stmt assign ") + case target != "" && strings.HasPrefix(line, " eval ") && strings.Contains(line, " -> "): + value := line[strings.LastIndex(line, " -> ")+len(" -> "):] + branch := strings.Trim(value, `"`) + if target == "x" { + branch = map[string]string{"1": "a", "2": "b", "3": "c"}[value] + } + if target == "x" || target == "order" { + step = append(step, write{target, value, branch[len(branch)-1:]}) + } + target = "" + } + c, ok := noted[line] + if !ok { + continue + } + took := c.Alternatives[c.Taken] + switch c.Kind { + case ChoiceTokenOrder: + if len(step) == 0 || !strings.HasSuffix(took, "@"+step[0].branch) { + t.Errorf("%s: %s: took %q, but the step's writes were %v", policy, c, took, step) + } + case ChoiceWriteOrder: + name := strings.SplitN(took, " := ", 2)[0] + last := "" + for _, w := range step { + if w.feature == name { + last = w.value + } + } + if !strings.HasPrefix(took, name+" := "+last+" ") { + t.Errorf("%s: %s: %q stood, but the step's last write of %s was %s", policy, c, took, name, last) + } + case ChoiceDecisionBranch: + want := map[string]string{"1": "1->warn", "2": "2->alarm"}[FormatTraceValue(outputs["handler"])] + if took != want { + t.Errorf("%s: %s: took %q, but handler = %s", policy, c, took, FormatTraceValue(outputs["handler"])) + } + } + } +} + // Under a seeded policy a state with two transitions enabled by one event fires // the one the seed picks, and the choice point names that one. func TestSeededTransitionChoiceMatchesTheRun(t *testing.T) { @@ -724,7 +787,7 @@ func changeWatchOrder(t *testing.T, policy SchedulePolicy) ([]int64, []ChoicePoi state waiting; accept when cell.mark > 0 then took; state took { - entry action take { assign seen := cell.mark; assign cell.mark := cell.mark + 1; } + entry action take { assign seen := cell.mark; then assign cell.mark := cell.mark + 1; } } } } diff --git a/internal/exec/runtime/state_change_trigger.go b/internal/exec/runtime/state_change_trigger.go index f5851da8da..9ee959b598 100644 --- a/internal/exec/runtime/state_change_trigger.go +++ b/internal/exec/runtime/state_change_trigger.go @@ -1,6 +1,7 @@ package runtime import ( + "errors" "fmt" "maps" "strings" @@ -129,7 +130,7 @@ func newChangePoll() *changePoll { // risenChanges polls the change conditions under a probe, keeping the latches, // and reports every transition whose rise would now dispatch, or fail (nil). -func (e *StateExecutor) risenChanges() ([]*lower.Transition, bool) { +func (e *StateExecutor) risenChanges() ([]*lower.Transition, bool, error) { defer e.ctx.beginProbe()() fired := maps.Clone(e.changeFired) defer func() { e.changeFired = fired }() @@ -137,7 +138,7 @@ func (e *StateExecutor) risenChanges() ([]*lower.Transition, bool) { e.changeRearmed = make(map[*lower.Transition]bool) defer func() { e.changeRearmed = nil }() if err := e.observeChangeConditions(poll); err != nil { - return nil, true + return nil, true, err } var risen []*lower.Transition for _, trans := range poll.observed { @@ -145,20 +146,23 @@ func (e *StateExecutor) risenChanges() ([]*lower.Transition, bool) { risen = append(risen, trans) } } - return risen, len(risen) > 0 + return risen, len(risen) > 0, nil } // risenChange polls the change conditions under a probe, keeping the latches, // and reports the first transition whose rise would now dispatch, or fail (nil). -func (e *StateExecutor) risenChange() (*lower.Transition, bool) { - risen, ok := e.risenChanges() +func (e *StateExecutor) risenChange() (*lower.Transition, bool, error) { + risen, ok, err := e.risenChanges() + if err != nil { + return nil, ok, err + } if !ok { - return nil, false + return nil, false, nil } if len(risen) == 0 { - return nil, true + return nil, true, nil } - return risen[0], true + return risen[0], true, nil } // riseEnables reports whether the poll's rise is an occurrence for trans, one a @@ -218,7 +222,14 @@ func (e *StateExecutor) observeChangeConditions(poll *changePoll) error { // The guard is read once per poll, alongside the condition, so which // transitions this rise enables does not depend on selection order. if decided { - poll.guard[trans] = e.probeChangeGuard(poll, source, transitions, i) + var unevaluable *UnevaluableGuard + var err error + if poll.guard[trans], unevaluable, err = e.probeChangeGuard(poll, source, transitions, i); err != nil { + return err + } + if unevaluable != nil { + poll.unevaluable[trans] = *unevaluable + } } else if poll.guard[trans], err = e.passesGuard(trans); err != nil { return fmt.Errorf("state %s: eval change guard: %w", source.Name, err) } @@ -237,29 +248,35 @@ func (e *StateExecutor) observeChangeConditions(poll *changePoll) error { // probeChangeGuard reads the guard of the transition at position i out of state // once an earlier one is enabled, as a probe the context undoes whole. One that // cannot be evaluated is not enabled, consumes nothing and is noted on the poll. -func (e *StateExecutor) probeChangeGuard(poll *changePoll, state *ast.StateNode, transitions []*lower.Transition, i int) bool { +func (e *StateExecutor) probeChangeGuard(poll *changePoll, state *ast.StateNode, transitions []*lower.Transition, i int) (bool, *UnevaluableGuard, error) { var pass bool var err error - e.preview(func() { pass, err = e.passesGuard(transitions[i]) }) + transition := transitions[i] + e.preview(func() { pass, err = e.passesGuard(transition) }) if err != nil { - poll.unevaluable[transitions[i]] = e.unevaluableTransition(state, transitions, i, fmt.Errorf("eval change guard: %w", err)) + if errors.Is(err, ErrOrderDependentPreview) || errors.Is(err, ErrOrderDependentGuardEffect) { + return false, nil, err + } + note := e.unevaluableTransition(state, transitions, i, fmt.Errorf("eval change guard: %w", err)) poll.wait(transitions[i], state.Name, "guard is not evaluable") - return false + return false, ¬e, nil } - return pass + return pass, nil, nil } // changeConditionHolds evaluates one change condition in the scope the // transition was written in, the machine's data shadowing it. func (e *StateExecutor) changeConditionHolds(changeEvent *ast.ChangeEvent, trans *lower.Transition) (bool, error) { - condVal, err := e.evalStepOf(trans.Source, changeEvent.Condition, trans.Scope) - if err != nil { - return false, fmt.Errorf("eval change condition: %w", err) - } - if condVal.Kind != ValConst || condVal.Const.Kind != semantics.ValBool { - return false, fmt.Errorf("change condition must be boolean, got %v", condVal.Kind) - } - return condVal.Const.Bool, nil + return e.ctx.guardUnderStatementOrders(changeEvent.Condition, e.ctx.enclosingExecutorStep(), trans.Scope, func() (bool, error) { + condVal, err := e.evalStepOf(trans.Source, changeEvent.Condition, trans.Scope) + if err != nil { + return false, fmt.Errorf("eval change condition: %w", err) + } + if condVal.Kind != ValConst || condVal.Const.Kind != semantics.ValBool { + return false, fmt.Errorf("change condition must be boolean, got %v", condVal.Kind) + } + return condVal.Const.Bool, nil + }) } // risenChangeTransitions returns the positions of the state's change-triggered @@ -274,7 +291,11 @@ func (e *StateExecutor) risenChangeTransitions(state *ast.StateNode, poll *chang continue } if poll.condition[trans] && !e.changeFired[trans] && poll.guard[trans] { - if e.routeAvailable(trans, occurrence) { + available, err := e.routeAvailable(trans, occurrence) + if err != nil { + return nil, nil, err + } + if available { enabled = append(enabled, i) } } diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index 30c28efb84..5a65be5ff5 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -1209,8 +1209,10 @@ func (e *StateExecutor) dispatchInOrder( defer func() { e.joinChosen = saved }() acted := false + var previewErr error gone := func(candidate dispatchCandidate) bool { return !e.isActive(candidate.leaf) || e.state.Ended() } // A guard that cannot be read is left to the firing, which reports the error. + // An order-dependent verdict instead fails the dispatch as not covered. void := func(candidate dispatchCandidate) bool { if gone(candidate) { return true @@ -1218,7 +1220,14 @@ func (e *StateExecutor) dispatchInOrder( var pass bool var err error e.preview(func() { pass, err = armed(candidate) }) - return err == nil && !pass + if err != nil { + if errors.Is(err, ErrOrderDependentPreview) || errors.Is(err, ErrOrderDependentGuardEffect) { + previewErr = err + return true + } + return false + } + return !pass } firing := func(candidate dispatchCandidate) error { if gone(candidate) { @@ -1249,7 +1258,10 @@ func (e *StateExecutor) dispatchInOrder( } f.spawnAt(head, func() error { return firing(candidate) }) } - return f.drain() + if err := f.drain(); err != nil { + return err + } + return previewErr }) return acted, err } @@ -1720,7 +1732,7 @@ func (e *StateExecutor) completionEnabled(trans *lower.Transition) (bool, error) if err != nil || !pass { return false, err } - return e.routeAvailable(trans, nil), nil + return e.routeAvailable(trans, nil) } // transitionDecided records what selecting the transition now firing noted, its @@ -1781,7 +1793,11 @@ func (e *StateExecutor) enabledTransitions(state *ast.StateNode, event *Event) ( var ok bool if len(enabled) > 0 { var unevaluable *UnevaluableGuard - if ok, unevaluable = e.probeTransition(state, transitions, i, event); unevaluable != nil { + var err error + if ok, unevaluable, err = e.probeTransition(state, transitions, i, event); err != nil { + return nil, nil, err + } + if unevaluable != nil { notes = append(notes, *unevaluable) } } else { @@ -1800,15 +1816,19 @@ func (e *StateExecutor) enabledTransitions(state *ast.StateNode, event *Event) ( // probeTransition reads whether the transition at position i out of state reacts // to event once another already does, as a probe the context undoes whole. One // that cannot be evaluated is not selected and is returned as the note to record. -func (e *StateExecutor) probeTransition(state *ast.StateNode, transitions []*lower.Transition, i int, event *Event) (bool, *UnevaluableGuard) { +func (e *StateExecutor) probeTransition(state *ast.StateNode, transitions []*lower.Transition, i int, event *Event) (bool, *UnevaluableGuard, error) { var ok bool var err error - e.preview(func() { ok, err = e.transitionEnabled(transitions[i], event) }) + transition := transitions[i] + e.preview(func() { ok, err = e.transitionEnabled(transition, event) }) if err != nil { + if errors.Is(err, ErrOrderDependentPreview) || errors.Is(err, ErrOrderDependentGuardEffect) { + return false, nil, err + } note := e.unevaluableTransition(state, transitions, i, err) - return false, ¬e + return false, ¬e, nil } - return ok, nil + return ok, nil, nil } // transitionEnabled reports whether trans reacts to event: its trigger and guard @@ -1831,7 +1851,7 @@ func (e *StateExecutor) transitionEnabled(trans *lower.Transition, event *Event) if err != nil || !pass { return false, err } - return e.routeAvailable(trans, event), nil + return e.routeAvailable(trans, event) } // transitionChoice is the transitions out of state enabled for one event, at @@ -2570,15 +2590,17 @@ func (e *StateExecutor) passesGuard(trans *lower.Transition) (bool, error) { if trans == nil || trans.Guard == nil { return true, nil } - val, err := e.evalTransitionStep(trans, trans.Guard, trans.BodyScope) - if err != nil { - return false, fmt.Errorf("eval guard of %s: %w", transitionDescription(trans), err) - } - if val.Kind != ValConst || val.Const.Kind != semantics.ValBool { - return false, fmt.Errorf("%w: guard of %s must be boolean, got %s", - ErrTypeMismatch, transitionDescription(trans), describeOperand(val)) - } - return val.Const.Bool, nil + return e.ctx.guardUnderStatementOrders(trans.Guard, e.ctx.enclosingExecutorStep(), trans.BodyScope, func() (bool, error) { + val, err := e.evalTransitionStep(trans, trans.Guard, trans.BodyScope) + if err != nil { + return false, fmt.Errorf("eval guard of %s: %w", transitionDescription(trans), err) + } + if val.Kind != ValConst || val.Const.Kind != semantics.ValBool { + return false, fmt.Errorf("%w: guard of %s must be boolean, got %s", + ErrTypeMismatch, transitionDescription(trans), describeOperand(val)) + } + return val.Const.Bool, nil + }) } // transitionDescription names a transition for a diagnostic: by the name it was @@ -3670,6 +3692,9 @@ func (e *StateExecutor) countDoStep() error { // a dispatch that acts is due, drawn against the move under ChoiceStepOrder — dispatches. func (e *StateExecutor) stepDue(due []*doAction, progress *dueProgress) (bool, error) { if dispatch := e.dueDispatch(); dispatch.acts { + if dispatch.fails != nil { + return false, dispatch.fails + } dispatchNow, err := e.chooseStepOrder(due, dispatch.step) if err != nil { return false, err @@ -3712,6 +3737,7 @@ type dueDispatch struct { tied []Event // the events tied at the head, the dispatch being the draw among them among []Event // the tied events whose dispatch acts: what a step order draws among acts bool // whether the dispatch takes its occurrence (eventActs) + fails error // a preview failure the selected dispatch must surface } // dueDispatch describes the dispatch due now; due is false when none is. @@ -3719,7 +3745,11 @@ func (e *StateExecutor) dueDispatch() dueDispatch { one := func(label string, acts bool) dueDispatch { return dueDispatch{due: true, label: label, step: label, acts: acts} } - if trans, risen := e.risenChange(); risen { + if trans, risen, err := e.risenChange(); err != nil { + d := one(dispatchPrefix+"change", true) + d.fails = err + return d + } else if risen { if trans == nil { return one("dispatch change", true) } @@ -3732,7 +3762,12 @@ func (e *StateExecutor) dueDispatch() dueDispatch { return dueDispatch{} } if tied := queue.Tied(); len(tied) >= 2 { - d := dueDispatch{due: true, label: dispatchTiedLabel, step: dispatchTiedLabel, tied: tied, among: e.actingEvents(tied)} + among, err := e.actingEvents(tied) + d := dueDispatch{due: true, label: dispatchTiedLabel, step: dispatchTiedLabel, tied: tied, among: among} + if err != nil { + d.fails = err + return d + } d.acts = len(d.among) > 0 if len(d.among) == 1 { d.step = dispatchPrefix + e.eventLabel(d.among[0]) @@ -3740,23 +3775,32 @@ func (e *StateExecutor) dueDispatch() dueDispatch { return d } head := queue.Peek() - d := one(dispatchPrefix+e.eventLabel(head), len(e.actingEvents([]Event{head})) > 0) + among, err := e.actingEvents([]Event{head}) + d := one(dispatchPrefix+e.eventLabel(head), len(among) > 0) + if err != nil { + d.fails = err + return d + } d.event = &head return d } // actingEvents previews which of the events a dispatch now would take (eventActs), in // order; an error in the preview counts as acting, the dispatch being where it surfaces. -func (e *StateExecutor) actingEvents(events []Event) []Event { +func (e *StateExecutor) actingEvents(events []Event) ([]Event, error) { var acting []Event + var err error e.preview(func() { for _, event := range events { - if ok, err := e.eventActs(event); err != nil || ok { + if ok, eventErr := e.eventActs(event); eventErr != nil { + err = eventErr + return + } else if ok { acting = append(acting, event) } } }) - return acting + return acting, err } // eventActs reports whether dispatching the event now would take it — fire a @@ -4969,14 +5013,16 @@ func (e *StateExecutor) entryGuardHolds(owner ast.Node, entry *lower.EntryTransi if entry.Guard == nil { return true, nil } - val, err := e.evalStepOf(e.bodyState(owner), entry.Guard, entry.Scope) - if err != nil { - return false, fmt.Errorf("eval guard of the entry transition into %s: %w", entry.Target.Name, err) - } - if val.Kind != ValConst || val.Const.Kind != semantics.ValBool { - return false, fmt.Errorf("guard of the entry transition into %s must be boolean, got %v", entry.Target.Name, val.Kind) - } - return val.Const.Bool, nil + return e.ctx.guardUnderStatementOrders(entry.Guard, e.ctx.enclosingExecutorStep(), entry.Scope, func() (bool, error) { + val, err := e.evalStepOf(e.bodyState(owner), entry.Guard, entry.Scope) + if err != nil { + return false, fmt.Errorf("eval guard of the entry transition into %s: %w", entry.Target.Name, err) + } + if val.Kind != ValConst || val.Const.Kind != semantics.ValBool { + return false, fmt.Errorf("guard of the entry transition into %s must be boolean, got %v", entry.Target.Name, val.Kind) + } + return val.Const.Bool, nil + }) } // bodyState is the state whose attributes a body's entry transitions read: the diff --git a/internal/exec/runtime/state_route.go b/internal/exec/runtime/state_route.go index 435128ae59..871b4449cf 100644 --- a/internal/exec/runtime/state_route.go +++ b/internal/exec/runtime/state_route.go @@ -181,7 +181,7 @@ func (e *StateExecutor) followOut(ps *ast.PseudostateNode, r route) (route, erro } // routeAvailable reports whether a transition's static route has a way through. -func (e *StateExecutor) routeAvailable(trans *lower.Transition, event *Event) bool { +func (e *StateExecutor) routeAvailable(trans *lower.Transition, event *Event) (bool, error) { var routeErr error e.preview(func() { unbind := func() {} // nothing to unbind until a trigger binds arguments @@ -213,7 +213,12 @@ func (e *StateExecutor) routeAvailable(trans *lower.Transition, event *Event) bo } _, routeErr = e.followOut(hist, route{}) }) - return !errors.Is(routeErr, errNoWayThrough) + if routeErr != nil && e.ctx.scheduling().ordersStatements() { + if errors.Is(routeErr, ErrOrderDependentPreview) || errors.Is(routeErr, ErrOrderDependentGuardEffect) { + return false, routeErr + } + } + return !errors.Is(routeErr, errNoWayThrough), nil } // settleDraws makes the draws the route is open at, in turn, once the transition @@ -322,7 +327,11 @@ func (e *StateExecutor) enabledBranches(ps *ast.PseudostateNode, outgoing []*low var pass bool if len(enabled) > 0 { var unevaluable *UnevaluableGuard - if pass, unevaluable = e.probeBranch(ps, outgoing, i); unevaluable != nil { + var err error + if pass, unevaluable, err = e.probeBranch(ps, outgoing, i); err != nil { + return nil, nil, fmt.Errorf("%s %s: %w", ps.Kind, ps.Name, err) + } + if unevaluable != nil { notes = append(notes, *unevaluable) } } else { @@ -378,11 +387,14 @@ func (e *StateExecutor) pickBranch(ps *ast.PseudostateNode, outgoing []*lower.Tr // probeBranch reads whether the branch at position i out of ps holds once // another already does, as a probe the context undoes whole; one that cannot be // evaluated is not enabled and is returned as the note to record. -func (e *StateExecutor) probeBranch(ps *ast.PseudostateNode, outgoing []*lower.Transition, i int) (bool, *UnevaluableGuard) { +func (e *StateExecutor) probeBranch(ps *ast.PseudostateNode, outgoing []*lower.Transition, i int) (bool, *UnevaluableGuard, error) { var pass bool var err error e.preview(func() { pass, err = e.passesGuard(outgoing[i]) }) if err != nil { + if errors.Is(err, ErrOrderDependentPreview) || errors.Is(err, ErrOrderDependentGuardEffect) { + return false, nil, err + } file, _ := e.transitionLocation(ps, outgoing[i]) return false, &UnevaluableGuard{ Where: pseudostateWhere(ps), @@ -390,9 +402,9 @@ func (e *StateExecutor) probeBranch(ps *ast.PseudostateNode, outgoing []*lower.T Reason: err.Error(), File: file, Span: outgoing[i].Guard.Span(), - } + }, nil } - return pass, nil + return pass, nil, nil } // branchPoint is the branches out of ps enabled, at their declared positions, as diff --git a/internal/exec/runtime/state_run_to_completion.go b/internal/exec/runtime/state_run_to_completion.go index 9c20ade189..998d50e697 100644 --- a/internal/exec/runtime/state_run_to_completion.go +++ b/internal/exec/runtime/state_run_to_completion.go @@ -202,6 +202,9 @@ func (e *StateExecutor) settleEntered(leaf *ast.StateNode) error { // entryStep chooses between free dispatch and held entry work. func (e *StateExecutor) entryStep(progress *dueProgress) (bool, error) { dispatch, free := e.dispatchFree(e.dueDispatch()) + if dispatch.fails != nil { + return false, dispatch.fails + } if !free && len(e.held) == 1 { item := e.held[0] e.held = slices.Delete(e.held, 0, 1) @@ -264,6 +267,9 @@ func (e *StateExecutor) dispatchFree(d dueDispatch) (dueDispatch, bool) { if !d.due || !d.acts { return d, false } + if d.fails != nil { + return d, true + } scopes := e.heldScopes() if len(scopes) == 0 { return d, true @@ -273,7 +279,12 @@ func (e *StateExecutor) dispatchFree(d dueDispatch) (dueDispatch, bool) { } free := make([]Event, 0, len(d.among)) for _, event := range d.among { - if !e.eventHeld(scopes, event) { + held, err := e.eventHeld(scopes, event) + if err != nil { + d.fails = err + return d, true + } + if !held { free = append(free, event) } } @@ -291,12 +302,30 @@ func (e *StateExecutor) dispatchFree(d dueDispatch) (dueDispatch, bool) { // noAmongFree reports whether a dispatch naming no candidate events may run: // its own event and every risen change must be free of the held scopes. func (e *StateExecutor) noAmongFree(d dueDispatch, scopes []*ast.StateNode) (dueDispatch, bool) { - if d.event != nil && e.eventHeld(scopes, *d.event) { - return d, false + if d.event != nil { + held, err := e.eventHeld(scopes, *d.event) + if err != nil { + d.fails = err + return d, true + } + if held { + return d, false + } } - if risen, ok := e.risenChanges(); ok { + if risen, ok, err := e.risenChanges(); err != nil { + d.fails = err + return d, true + } else if ok { for _, trans := range risen { - if trans != nil && e.eventHeld(scopes, Event{Payload: trans}) { + if trans == nil { + continue + } + held, err := e.eventHeld(scopes, Event{Payload: trans}) + if err != nil { + d.fails = err + return d, true + } + if held { return d, false } } @@ -306,9 +335,9 @@ func (e *StateExecutor) noAmongFree(d dueDispatch, scopes []*ast.StateNode) (due // eventHeld reports whether event's transitions fall inside a held scope; // a selection that fails to preview is held rather than risked. -func (e *StateExecutor) eventHeld(scopes []*ast.StateNode, event Event) bool { +func (e *StateExecutor) eventHeld(scopes []*ast.StateNode, event Event) (bool, error) { if trans, ok := event.Payload.(*lower.Transition); ok { - return scopeContains(e.graph, scopes, e.transitionOwner(trans)) + return scopeContains(e.graph, scopes, e.transitionOwner(trans)), nil } var candidates []dispatchCandidate var err error @@ -316,16 +345,16 @@ func (e *StateExecutor) eventHeld(scopes []*ast.StateNode, event Event) bool { candidates, err = e.selectTransitions(&event) }) if err != nil { - return true + return false, err } for _, candidate := range candidates { for _, index := range candidate.enabled { if scopeContains(e.graph, scopes, e.graph.Transitions[candidate.source][index].Owner) { - return true + return true, nil } } } - return false + return false, nil } func (e *StateExecutor) transitionOwner(trans *lower.Transition) *ast.StateNode { diff --git a/internal/exec/runtime/state_space.go b/internal/exec/runtime/state_space.go index 3a47bf981f..91e27e4082 100644 --- a/internal/exec/runtime/state_space.go +++ b/internal/exec/runtime/state_space.go @@ -599,15 +599,17 @@ func (e *ActionExecutor) crossingTerminal(crossing lower.ZeroCrossing) (bool, er if crossing.Terminal == nil { return false, nil } - val, err := e.evalCrossingExpr(crossing.Terminal, crossing.TerminalScope) - if err != nil { - return false, fmt.Errorf("terminal of zero crossing %s of action %s: %w", crossing.Name, symbolText(e.action), err) - } - if val.Kind != ValConst || val.Const.Kind != semantics.ValBool { - return false, fmt.Errorf("%w: terminal of zero crossing %s of action %s is %s, not a Boolean", - ErrStateSpaceValue, crossing.Name, symbolText(e.action), describeValue(val)) - } - return val.Const.Bool, nil + return e.ctx.guardUnderStatementOrders(crossing.Terminal, e.stepCount+1, crossing.TerminalScope, func() (bool, error) { + val, err := e.evalCrossingExpr(crossing.Terminal, crossing.TerminalScope) + if err != nil { + return false, fmt.Errorf("terminal of zero crossing %s of action %s: %w", crossing.Name, symbolText(e.action), err) + } + if val.Kind != ValConst || val.Const.Kind != semantics.ValBool { + return false, fmt.Errorf("%w: terminal of zero crossing %s of action %s is %s, not a Boolean", + ErrStateSpaceValue, crossing.Name, symbolText(e.action), describeValue(val)) + } + return val.Const.Bool, nil + }) } // evalCrossingExpr evaluates an expression of a crossing in the scope it was diff --git a/internal/exec/runtime/state_statements.go b/internal/exec/runtime/state_statements.go index 1c79a19269..6705b9b5c8 100644 --- a/internal/exec/runtime/state_statements.go +++ b/internal/exec/runtime/state_statements.go @@ -212,7 +212,8 @@ func (e *StateExecutor) newDoRun(behavior lower.StateBehavior, firing *firing) * return nil } host := e.behaviorHost(behavior, firing) - body := &bodyRun{work: host, awaitsMessages: true, yields: true, steps: e.ctx.scheduling().oneMove()} + oneMove := e.ctx.scheduling().oneMove() + body := &bodyRun{work: host, awaitsMessages: true, yields: true, steps: oneMove, guards: oneMove, nodesYield: true} return &doRun{host: host, body: body} } @@ -419,6 +420,23 @@ func (h *stateStmtHost) materializeOccurrence() (*Instance, error) { } // acceptReturn rejects a `return`: a state behavior computes no result. +// statementOrder is how stmts may be ordered where the schedule picks it: an inline +// body's statements are subactions of the behavior's performance no succession orders. +func (h *stateStmtHost) statementOrder(stmts []lower.Statement) *lower.StatementOrder { + if len(stmts) < 2 || !h.exec.ctx.scheduling().ordersStatements() { + return nil + } + graph := h.flow.graph + if graph == nil { + graph = &lower.ActionGraph{Scope: h.behavior.Scope} + } + return h.perfs.statementOrder(graph, h.behavior.Node, stmts) +} + +func (h *stateStmtHost) orderStep() int { return 0 } + +func (h *stateStmtHost) yieldsBetweenStatements() bool { return true } + func (h *stateStmtHost) acceptReturn(Value, lower.Return) error { return fmt.Errorf("%w: %s", ErrReturnOutsideCalc, h.describe()) } diff --git a/internal/exec/runtime/statement_order_sweep.go b/internal/exec/runtime/statement_order_sweep.go new file mode 100644 index 0000000000..70506f8729 --- /dev/null +++ b/internal/exec/runtime/statement_order_sweep.go @@ -0,0 +1,132 @@ +package runtime + +import ( + "errors" + "fmt" + "slices" +) + +const maxStatementOrderEvaluations = 1024 + +var ErrStatementOrderSweepLimit = errors.New("statement-order evaluation limit exceeded") + +type statementOrderSweep struct { + prefix []int + alternatives []int + taken []int + choices []ChoiceTaken +} + +func (s *statementOrderSweep) choose(choice *ChoicePoint) int { + position := len(s.alternatives) + s.alternatives = append(s.alternatives, len(choice.Alternatives)) + taken := 0 + if position < len(s.prefix) { + taken = s.prefix[position] + } + if taken < 0 || taken >= len(choice.Alternatives) { + taken = 0 + } + s.taken = append(s.taken, taken) + choice.Taken = taken + s.choices = append(s.choices, choice.Choice()) + return taken +} + +func (ctx *Context) sweepStatementOrders(eval func() error) error { + return ctx.sweepStatementOrderVariants(func(*statementOrderSweep) error { return eval() }) +} + +func (ctx *Context) sweepStatementOrderVariants(eval func(*statementOrderSweep) error) error { + if ctx.statementOrderSweep != nil { + return eval(ctx.statementOrderSweep) + } + return ctx.sweepStatementOrderVariantsNested(eval) +} + +func (ctx *Context) sweepStatementOrderVariantsNested(eval func(*statementOrderSweep) error) error { + previous := ctx.statementOrderSweep + pending := [][]int{{}} + seen := map[string]bool{"[]": true} + evaluations := 0 + for len(pending) > 0 { + if evaluations == maxStatementOrderEvaluations { + return fmt.Errorf("%w: at most %d statement orders", ErrStatementOrderSweepLimit, maxStatementOrderEvaluations) + } + last := len(pending) - 1 + prefix := pending[last] + pending = pending[:last] + sweep := &statementOrderSweep{prefix: prefix} + ctx.statementOrderSweep = sweep + restore := ctx.beginProbe() + err := eval(sweep) + restore() + ctx.statementOrderSweep = previous + evaluations++ + if err != nil { + return err + } + for position := len(sweep.alternatives) - 1; position >= 0; position-- { + for alternative := sweep.alternatives[position] - 1; alternative > 0; alternative-- { + next := append(slices.Clone(sweep.taken[:position]), alternative) + key := fmt.Sprint(next) + if !seen[key] { + seen[key] = true + pending = append(pending, next) + } + } + } + } + return nil +} + +func (ctx *Context) sweepStatementOrdersAt(choices []ChoiceTaken, eval func() error) error { + if ctx.statementOrderSweep != nil { + return eval() + } + prefix := make([]int, len(choices)) + for i, choice := range choices { + prefix[i] = choice.Taken + } + sweep := &statementOrderSweep{prefix: prefix} + previous := ctx.statementOrderSweep + ctx.statementOrderSweep = sweep + restore := ctx.beginProbe() + err := eval() + restore() + ctx.statementOrderSweep = previous + if err != nil { + return err + } + if len(sweep.choices) != len(choices) { + return fmt.Errorf("the final statement orders do not match the replay witness") + } + for i, choice := range choices { + got := sweep.choices[i] + if got.Kind != choice.Kind || got.Step != choice.Step || + got.Where != choice.Where || got.Alternatives != choice.Alternatives || + got.Took != choice.Took || !slices.Equal(got.Among, choice.Among) { + return fmt.Errorf("final statement order %d does not match the replay witness", i+1) + } + } + return nil +} + +func (ctx *Context) everyStatementOrder(eval func() (bool, error)) (bool, error) { + if ctx.statementOrderSweep != nil { + return eval() + } + holds := true + err := ctx.sweepStatementOrders(func() error { + result, err := eval() + if err != nil { + holds = false + return err + } + if !result { + holds = false + } + return nil + }) + return holds, err +} diff --git a/internal/exec/runtime/statements.go b/internal/exec/runtime/statements.go index 7c4594f872..39f24c0c69 100644 --- a/internal/exec/runtime/statements.go +++ b/internal/exec/runtime/statements.go @@ -47,10 +47,14 @@ func (env *stmtEnv) leave() { } } -// declareUnvalued marks a name the innermost entered block declares without a value. +// declareUnvalued marks a name the innermost entered block declares without a +// value — or the body's own data when no block is entered, which marks it +// declared for a write while a read answers as missing, like a feature stated +// without a value. func (env *stmtEnv) declareUnvalued(name string) { depth := len(env.frames) if depth == 0 { + env.data.markUnvalued(name) return } if env.unvalued[depth-1] == nil { @@ -160,6 +164,15 @@ type stmtHost interface { // assignment binds that output for this activation rather than writing a value // the body merely holds. declaredOutput(name string) bool + // statementOrder is how stmts, the host's body or a block in it, may be + // ordered under the run's schedule; nil keeps declaration order. + statementOrder(stmts []lower.Statement) *lower.StatementOrder + // orderStep is the step a statement order is chosen in, as the run's other + // choices of the step name it. + orderStep() int + // yieldsBetweenStatements reports whether this list participates in its + // enclosing action body's interleaving. + yieldsBetweenStatements() bool // acceptReturn takes the value a `return` yields. acceptReturn(value Value, s lower.Return) error // effect states an effect on the world outside the body, over engine's values. @@ -201,6 +214,10 @@ type stmtEngine struct { frameBuf []frame // thisOccurrence is host.materializeOccurrence, bound once for every evalIn. thisOccurrence func() (*Instance, error) + // features are the valued features a body's statements may name where the + // host supplies them — a constraint's parameters — nil where the body's + // frame answers every name already. + features map[string]scopedExpr } // newStmtEngineOver returns an engine running statements against data, which also @@ -258,6 +275,7 @@ func (e *stmtEngine) evalIn(scope *symbols.Scope) *EvalContext { thisOccurrence: e.thisOccurrence, frames: frames, trace: e.ctx.trace, + features: e.features, inBehaviorBody: true, activation: e.activation, } @@ -303,26 +321,114 @@ type stmtListFrame struct { i int run *runState elements int64 + order *lower.StatementOrder + // done and blocked track an unordered list's statements (lower.StatementOrder); + // i is -1 between two of them. divided is whether another performance may run between. + done, blocked []bool + divided bool + // strands holds, by position, each statement started and set aside at an inner + // boundary so a statement it does not commute with runs meanwhile; switched is + // the one just set aside, -1 for none, and levels the trace levels i opened under. + strands []*stmtStrand + switched int + levels int } -func (f *stmtListFrame) abandon(*Context) { f.run.elements = f.elements } +// stmtStrand is a statement of an unordered list set aside mid-way: the frames it +// paused at, innermost first, the trace levels and elements it holds, and why it paused. +type stmtStrand struct { + cursor []bodyFrame + levels int + run *runState + held int64 + paused bodyPause +} + +func (s *stmtStrand) clone() *stmtStrand { + c := *s + c.cursor = make([]bodyFrame, len(s.cursor)) + for i, f := range s.cursor { + c.cursor[i] = f.clone() + } + return &c +} + +// abandon gives back the elements the paused statement held; one yielded before +// its next statement has none open. The statements set aside are abandoned too. +func (f *stmtListFrame) abandon(ctx *Context) { + if f.run != nil && (f.i >= 0 || f.strands == nil) { + f.run.elements = f.elements + } + f.abandonStrands(ctx) +} + +// abandonStrands ends what the statements set aside hold open, innermost first. +func (f *stmtListFrame) abandonStrands(ctx *Context) { + for i, s := range f.strands { + if s == nil { + continue + } + for _, inner := range s.cursor { + inner.abandon(ctx) + } + if s.run != nil { + s.run.elements -= s.held + } + f.strands[i] = nil + } +} -func (f *stmtListFrame) clone() bodyFrame { c := *f; return &c } +func (f *stmtListFrame) clone() bodyFrame { + c := *f + c.done, c.blocked = slices.Clone(f.done), slices.Clone(f.blocked) + if f.strands != nil { + c.strands = make([]*stmtStrand, len(f.strands)) + for i, s := range f.strands { + if s != nil { + c.strands[i] = s.clone() + } + } + } + return &c +} // run executes statements in declaration order, stopping at a `return`; a body // pausing in one is re-entered at that statement, one yielding between two at // the next. func (e *stmtEngine) run(stmts []lower.Statement) (stmtFlow, error) { + return e.runWithOrder(stmts, nil) +} + +func (e *stmtEngine) runWithOrder(stmts []lower.Statement, explicitOrder *lower.StatementOrder) (stmtFlow, error) { f, resumed, err := popFrame[*stmtListFrame](e.ctx) if err != nil { return flowNext, err } + order := explicitOrder + if order == nil && resumed { + order = f.order + } + if order == nil { + order = e.host.statementOrder(stmts) + } if !resumed { f = &stmtListFrame{} + divided := e.host.yieldsBetweenStatements() && e.ctx.body != nil && e.ctx.body.yields + if order != nil && order.Reorders(divided) { + f.i, f.done, f.blocked = -1, make([]bool, len(stmts)), make([]bool, len(stmts)) + f.divided, f.switched = divided, -1 + } } + f.order = order resumed = resumed && !e.ctx.yieldedHere() + if f.done != nil { + return e.runUnordered(stmts, f, resumed) + } for ; f.i < len(stmts); f.i++ { - if err := e.ctx.yieldBody(); err != nil { + if order != nil && order.Skipped(f.i) { + continue + } + if err := e.yieldBody(); err != nil { return flowNext, e.ctx.pausing(f, err) } flow, err := e.statement(stmts[f.i], f, resumed) @@ -330,11 +436,160 @@ func (e *stmtEngine) run(stmts []lower.Statement) (stmtFlow, error) { if err != nil || flow == flowReturn { return flow, e.ctx.pausing(f, err) } - e.ctx.bodyPerformed() + e.bodyPerformed() } return flowNext, nil } +// runUnordered executes statements no succession orders, each next one as the +// run's schedule picks among those lower.StatementOrder lets run next. A statement +// started is set aside at an inner boundary where one it does not commute with may +// run between its moves, so that one's moves may fall between them. +func (e *stmtEngine) runUnordered(stmts []lower.Statement, f *stmtListFrame, resumed bool) (flow stmtFlow, err error) { + order := f.order + if order == nil { + order = e.host.statementOrder(stmts) + } + var level *listLevel + if e.host.yieldsBetweenStatements() { + level = e.ctx.enterList(f, order) + } + defer e.ctx.leaveList(level) + defer func() { + if err != nil && !paused(err) { + f.abandonStrands(e.ctx) + } + }() + for { + if f.i < 0 { + next := f.candidates(order, !e.ctx.scheduling().ordersStatements()) + if len(next) == 0 { + return flowNext, nil + } + if err := e.yieldBody(); err != nil { + return flowNext, e.ctx.pausing(f, err) + } + f.i, f.switched = e.pickStatement(stmts, next), -1 + if e.host.yieldsBetweenStatements() { + resumed = e.ctx.resumeStrand(f) + } else { + resumed = false + } + if level != nil { + level.moved = false + } + } + flow, err := e.statement(stmts[f.i], f, resumed) + resumed = false + if paused(err) && e.ctx.body != nil && e.ctx.body.paused.strand == f { + e.ctx.setAside(f) + continue + } + if err != nil || flow == flowReturn { + return flow, e.ctx.pausing(f, err) + } + order.Ran(f.i, f.done, f.blocked, f.divided) + f.i = -1 + e.bodyPerformed() + } +} + +// candidates lists, ascending, the statements the list may run or go on with next: +// after one was set aside, it or those it does not commute with; else those +// lower.StatementOrder lets start next, with every one set aside. +func (f *stmtListFrame) candidates(order *lower.StatementOrder, fixed bool) []int { + var next []int + if f.switched >= 0 { + next = append(order.Rivals(f.switched, f.done, f.divided), f.switched) + } else if fixed { + next = order.NextFixed(f.done, f.blocked, f.divided) + } else { + next = order.Next(f.done, f.blocked, f.divided) + for i, s := range f.strands { + if s != nil { + next = append(next, i) + } + } + } + slices.Sort(next) + return slices.Compact(next) +} + +// setAside keeps the frames of f's statement, paused at an inner boundary, as a +// strand of f, closing the trace levels it holds while the list goes on. +func (ctx *Context) setAside(f *stmtListFrame) { + run := ctx.body + if f.strands == nil { + f.strands = make([]*stmtStrand, len(f.done)) + } + s := &stmtStrand{ + cursor: run.cursor, + levels: ctx.bodyLevels() - f.levels, + run: f.run, + paused: bodyPause{yielded: true}, + } + if f.run != nil { + s.held = f.run.elements - f.elements + } + f.strands[f.i] = s + run.cursor, run.paused = nil, bodyPause{} + ctx.trace.setNesting(run.traceBase + f.levels) + f.switched, f.i = f.i, -1 +} + +// resumeStrand readies f's statement i to go on where it was set aside, reporting +// whether it was: its frames resume and its trace levels reopen. +func (ctx *Context) resumeStrand(f *stmtListFrame) bool { + f.levels = ctx.bodyLevels() + if f.strands == nil || f.strands[f.i] == nil { + return false + } + s := f.strands[f.i] + f.strands[f.i] = nil + run := ctx.body + run.resuming, run.paused = s.cursor, s.paused + f.run = s.run + if s.run != nil { + f.elements = s.run.elements - s.held + } + ctx.trace.setNesting(run.traceBase + f.levels + s.levels) + return true +} + +// statementsWherePrefix opens where a statement order names the body it was made in. +const statementsWherePrefix = "statements in " + +// pickStatement picks the statement to run next among next, two or more a +// choice point of the run's schedule. +func (e *stmtEngine) pickStatement(stmts []lower.Statement, next []int) int { + if len(next) == 1 { + return next[0] + } + if !e.ctx.scheduling().ordersStatements() { + return next[0] + } + alts := make([]string, len(next)) + for k, i := range next { + alts[k] = fmt.Sprintf("%d %s", i+1, stmtLabel(stmts[i])) + } + choice := ChoicePoint{ + Kind: ChoiceStatementOrder, + Step: e.host.orderStep(), + Where: statementsWherePrefix + e.host.describe(), + Alternatives: alts, + } + if e.ctx.statementOrderSweep != nil { + choice.Taken = e.ctx.statementOrderSweep.choose(&choice) + if e.ctx.statementOrderGuardBodies != nil { + e.ctx.statementOrderGuardBodies[e.host.describe()] = true + } + return next[choice.Taken] + } + choice.Taken = e.ctx.scheduling().choose(choice, nil) + e.ctx.noteChoice(choice) + return next[choice.Taken] +} + // statement executes one lowered statement, recording it in the trace with the // evaluations and nested statements it produces underneath it; one paused keeps // its trace level and elements open until it is resumed and ends. @@ -411,7 +666,6 @@ func (e *stmtEngine) execute(stmt lower.Statement) (stmtFlow, error) { } return flowNext, e.host.assignOuter(e.env, s.Target, value, s) case lower.Declare: - value := Value{Kind: ValNull} if s.Value != nil { evaluated, err := e.evalIn(s.Scope).Eval(s.Value) if err != nil { @@ -420,9 +674,17 @@ func (e *stmtEngine) execute(stmt lower.Statement) (stmtFlow, error) { if err := e.ctx.checkBodyDeclaration(s.Scope, e.host.describe(), s.Name, &evaluated); err != nil { return flowNext, err } - value = evaluated + e.env.declare(s.Name, evaluated) + return flowNext, nil + } + // A constraint body's performance declares a valueless name as missing + // until a step binds it — a read answers as missing, a write binds it; + // every other body binds null for it, as it always has. + if _, constraint := e.host.(*constraintStmtHost); constraint { + e.env.declareUnvalued(s.Name) + } else { + e.env.declare(s.Name, Value{Kind: ValNull}) } - e.env.declare(s.Name, value) return flowNext, nil case lower.DeclareUsage: return flowNext, e.declareUsage(s) @@ -515,6 +777,7 @@ func (e *stmtEngine) ifStatement(stmt lower.If) (stmtFlow, error) { return flowNext, nil } f = &branchFrame{elseBranch: !holds} + e.ctx.guardPerformed() } branch := stmt.Then if f.elseBranch { @@ -645,7 +908,7 @@ func (e *stmtEngine) blockFlow(block lower.Block) (stmtFlow, error) { } resumed = resumed && !e.ctx.yieldedHere() for f.node != nil { - if err := e.ctx.yieldBody(); err != nil { + if err := e.yieldBody(); err != nil { return flowNext, e.ctx.pausing(f, err) } // A node reached spends a step, so a flow that does not end fails the run. @@ -659,7 +922,7 @@ func (e *stmtEngine) blockFlow(block lower.Block) (stmtFlow, error) { if err != nil || flow == flowReturn { return flow, e.ctx.pausing(f, err) } - e.ctx.bodyPerformed() + e.bodyPerformed() successors := graph.Edges[f.node] if len(successors) == 0 { return flowNext, nil @@ -669,6 +932,19 @@ func (e *stmtEngine) blockFlow(block lower.Block) (stmtFlow, error) { return flowNext, nil } +func (e *stmtEngine) yieldBody() error { + if !e.host.yieldsBetweenStatements() { + return nil + } + return e.ctx.yieldBody() +} + +func (e *stmtEngine) bodyPerformed() { + if e.host.yieldsBetweenStatements() { + e.ctx.bodyPerformed() + } +} + // blockNode runs one node of a block's flow: the host performs an action usage in // a frame of its own; a run of statements runs in the frame the block entered. // A node resumed keeps the trace level it opened. @@ -792,7 +1068,7 @@ func (e *stmtEngine) loop(stmt lower.Loop) (stmtFlow, error) { defer leave() for { - if err := e.ctx.yieldBody(); err != nil { + if err := e.yieldBody(); err != nil { return flowNext, e.ctx.pausing(f, err) } if !resumed { @@ -805,7 +1081,7 @@ func (e *stmtEngine) loop(stmt lower.Loop) (stmtFlow, error) { if err != nil || done || flow == flowReturn { return flow, e.ctx.pausing(f, err) } - e.ctx.bodyPerformed() + e.bodyPerformed() } } @@ -871,7 +1147,7 @@ func (e *stmtEngine) forLoop(stmt lower.Loop) (stmtFlow, error) { } f = &loopFrame{elements: elements} if len(elements) == 0 { - e.ctx.bodyPerformed() + e.bodyPerformed() } } resumed = resumed && !e.ctx.yieldedHere() @@ -879,7 +1155,7 @@ func (e *stmtEngine) forLoop(stmt lower.Loop) (stmtFlow, error) { defer leave() for f.iteration < len(f.elements) || resumed { - if err := e.ctx.yieldBody(); err != nil { + if err := e.yieldBody(); err != nil { return flowNext, e.ctx.pausing(f, err) } if !resumed { @@ -892,7 +1168,7 @@ func (e *stmtEngine) forLoop(stmt lower.Loop) (stmtFlow, error) { if err != nil || flow == flowReturn { return flow, e.ctx.pausing(f, err) } - e.ctx.bodyPerformed() + e.bodyPerformed() } return flowNext, nil } diff --git a/internal/exec/runtime/testdata/check/reduction_expected.txt b/internal/exec/runtime/testdata/check/reduction_expected.txt index e73cef18c1..bffc40f2de 100644 --- a/internal/exec/runtime/testdata/check/reduction_expected.txt +++ b/internal/exec/runtime/testdata/check/reduction_expected.txt @@ -4,9 +4,9 @@ # TestCheckReductionIsSound passing. 76 87 76 87 por_shared_write 28 34 28 41 por_guard_read -19 18 20 20 por_trigger_read -26 34 29 44 por_send_accept -40 42 43 63 por_join +40 46 40 47 por_trigger_read +44 71 44 78 por_send_accept +181 367 183 479 por_join 18 21 18 21 por_dynamic_target 24 40 85 220 por_independent_branches 44 78 123 306 por_alias @@ -16,7 +16,7 @@ 13 13 30 38 por_state_independent 17 18 17 18 por_state_effect_write 17 18 17 18 por_state_guard_read -21 22 21 22 por_state_do_write +34 36 34 36 por_state_do_write 9 9 9 9 por_state_send_accept 72 72 72 72 por_state_join_exit 26 24 26 24 por_state_join_guard diff --git a/internal/exec/runtime/testdata/conformance/accept_payload_nested_body.sysml b/internal/exec/runtime/testdata/conformance/accept_payload_nested_body.sysml index cacda36f07..8a0d74da91 100644 --- a/internal/exec/runtime/testdata/conformance/accept_payload_nested_body.sysml +++ b/internal/exec/runtime/testdata/conformance/accept_payload_nested_body.sysml @@ -15,10 +15,8 @@ package test { action receiver accept msg : Integer; - action processor { - if msg > 5 { assign total := msg * 2; } - while total < 20 { assign total := total + msg; } - } + action processor if msg > 5 { assign total := msg * 2; } + then while total < 20 { assign total := total + msg; } done; diff --git a/internal/exec/runtime/testdata/conformance/action_accept_if_branch.sysml b/internal/exec/runtime/testdata/conformance/action_accept_if_branch.sysml index 339a3ba32f..8460da0fc6 100644 --- a/internal/exec/runtime/testdata/conformance/action_accept_if_branch.sysml +++ b/internal/exec/runtime/testdata/conformance/action_accept_if_branch.sysml @@ -12,7 +12,7 @@ package test { } else { accept skippedThen : Integer; } - if false { + then if false { accept skippedElse : Integer; } else { accept elseValue : Integer; diff --git a/internal/exec/runtime/testdata/conformance/action_accept_if_branch.trace.golden b/internal/exec/runtime/testdata/conformance/action_accept_if_branch.trace.golden index 043f7f685e..4ae197dac1 100644 --- a/internal/exec/runtime/testdata/conformance/action_accept_if_branch.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_accept_if_branch.trace.golden @@ -1,14 +1,15 @@ step 1: token 1@split step 2: token 2@reader, token 3@gate1 +enter action node: reader +choice step 3: tokens 2@reader, 3@gate1 (unordered; took 3@gate1 first) +step 3: token 2@if, token 3@gate2 stmt if eval literal true -> true enter action node: branch body of action node reader -choice step 3: tokens 2@reader, 3@gate1 (unordered; took 3@gate1 first) -step 3: token 2@reader, token 3@gate2, token 4@usage_action -choice step 4: tokens 2@reader, 3@gate2 (unordered; took 3@gate2 first) -step 4: token 2@reader, token 3@gate3, token 4@usage_action -choice step 5: tokens 2@reader, 3@gate3 (unordered; took 3@gate3 first) -step 5: token 2@reader, token 3@sender, token 4@usage_action +choice step 4: tokens 2@if, 3@gate2 (unordered; took 3@gate2 first) +step 4: token 2@if, token 3@gate3, token 4@usage_action +choice step 5: tokens 2@if, 3@gate3 (unordered; took 3@gate3 first) +step 5: token 2@if, token 3@sender, token 4@usage_action stmt send eval literal 3 -> 3 stmt send @@ -16,6 +17,8 @@ stmt send stmt assign received eval feature thenValue -> 3 leave action node: branch body of action node reader +choice step 6: tokens 2@if, 3@sender (unordered; took 3@sender first) +step 6: token 2@if, token 3@sync stmt if eval literal false -> false enter action node: branch body of action node reader @@ -24,7 +27,7 @@ enter action node: branch body of action node reader eval feature elseValue -> 4 eval operator + -> 7 leave action node: branch body of action node reader -choice step 6: tokens 2@reader, 3@sender (unordered; took 3@sender first) -step 6: token 2@sync, token 3@sync -step 7: token 6@done -step 8: no active tokens +leave action node: reader +step 7: token 2@sync, token 3@sync +step 8: token 6@done +step 9: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_body_flow_terminate_node.sysml b/internal/exec/runtime/testdata/conformance/action_body_flow_terminate_node.sysml index 986673407c..adc92c0877 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_flow_terminate_node.sysml +++ b/internal/exec/runtime/testdata/conformance/action_body_flow_terminate_node.sysml @@ -8,8 +8,8 @@ package test { for i in 1..3 { action a { assign sum := sum + i; - terminate; - assign sum := sum + 1000; + then terminate; + then assign sum := sum + 1000; } action b { assign sum := sum + 1; } succession a then b; diff --git a/internal/exec/runtime/testdata/conformance/action_body_flow_terminate_statement.sysml b/internal/exec/runtime/testdata/conformance/action_body_flow_terminate_statement.sysml index 504ecaa5d2..ca1898b5d2 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_flow_terminate_statement.sysml +++ b/internal/exec/runtime/testdata/conformance/action_body_flow_terminate_statement.sysml @@ -6,12 +6,10 @@ package test { attribute after : Integer = 0; attribute completed : Integer = 0; first start; - then action iterate { - for i in 1..3 { - action a { assign sum := sum + i; } then terminate; - } - assign after := 1; + then action iterate for i in 1..3 { + action a { assign sum := sum + i; } then terminate; } + then assign after := 1; then action finish { assign completed := 1; } then done; } diff --git a/internal/exec/runtime/testdata/conformance/action_body_local_calc_usage.sysml b/internal/exec/runtime/testdata/conformance/action_body_local_calc_usage.sysml index 6fda79f390..40bb6b09e3 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_local_calc_usage.sysml +++ b/internal/exec/runtime/testdata/conformance/action_body_local_calc_usage.sysml @@ -19,13 +19,13 @@ package test { first start; action compute { - assign v := 2.0; calc t : Twice { in k = v; } - assign doubled := t.d; + assign v := 2.0; + then assign doubled := t.d; while i < 3 { - assign i := i + 1; calc step : Twice { in k = i; } - assign acc := acc + step.d; + assign i := i + 1; + then assign acc := acc + step.d; } } diff --git a/internal/exec/runtime/testdata/conformance/action_body_node_terminate.sysml b/internal/exec/runtime/testdata/conformance/action_body_node_terminate.sysml index 1ade2b136f..5639622690 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_node_terminate.sysml +++ b/internal/exec/runtime/testdata/conformance/action_body_node_terminate.sysml @@ -11,8 +11,8 @@ package test { for i in 1..3 { action a { assign sum := sum + i; - terminate; - assign sum := sum + 1000; + then terminate; + then assign sum := sum + 1000; } assign after := after + 1; } diff --git a/internal/exec/runtime/testdata/conformance/action_body_quantity_descent.sysml b/internal/exec/runtime/testdata/conformance/action_body_quantity_descent.sysml index 671e5a92ec..c6d1fec6ff 100644 --- a/internal/exec/runtime/testdata/conformance/action_body_quantity_descent.sysml +++ b/internal/exec/runtime/testdata/conformance/action_body_quantity_descent.sysml @@ -20,8 +20,8 @@ package test { action integrate { while h > 0.0 [m] { assign v := v - g * dt; - assign h := h + v * dt; - assign t := t + dt; + then assign h := h + v * dt; + then assign t := t + dt; } } diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.check.expected.json new file mode 100644 index 0000000000..6570ffbe0f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"c": ["1", "2"]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.declared.trace.golden new file mode 100644 index 0000000000..0b8e1f4a62 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.declared.trace.golden @@ -0,0 +1,17 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +eval feature c -> 0 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +eval feature c -> 1 +stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +choice step 3: writes c := 1 by token 2, c := 2 by token 3 (unordered; c := 2 by token 3 stood) +choice step 3: tokens 2@a, 3@b (unordered; took 2@a first) +step 3: token 2@j, token 3@j +step 4: token 4@done +step 5: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.expected.json new file mode 100644 index 0000000000..d3a54f2344 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"c": {"type": "Integer", "value": 1}}}, + {"outputs": {"c": {"type": "Integer", "value": 2}}} + ], + "admissible": "A leaf body's start shot and its assignments: another performance may run between them" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.seed-1.trace.golden new file mode 100644 index 0000000000..40e3717825 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.seed-1.trace.golden @@ -0,0 +1,19 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +eval feature c -> 0 +eval feature c -> 0 +choice step 3: tokens 2@a, 3@b (unordered; took 2@a first) +step 3: token 2@a, token 3@b +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +choice step 4: writes c := 1 by token 2, c := 1 by token 3 (unordered; c := 1 by token 3 stood) +choice step 4: tokens 2@a, 3@b (unordered; took 2@a first) +step 4: token 2@j, token 3@j +step 5: token 4@done +step 6: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.sysml b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.sysml new file mode 100644 index 0000000000..5d8550048b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.sysml @@ -0,0 +1,25 @@ +package test { + private import ScalarValues::*; + + action def ForkPlain { + attribute c : Integer := 0; + // Oracle (docs/project/behavior-semantic-oracle.md): two fork branches with one + // leaf body each; either may start between the other's snapshot and its write. + first start; + then fork f; + then a; + then b; + action a { + attribute t : Integer := c; + assign c := t + 1; + } + action b { + attribute t : Integer := c; + assign c := t + 1; + } + succession a then j; + succession b then j; + join j; + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.trace.golden new file mode 100644 index 0000000000..c046300163 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.trace.golden @@ -0,0 +1,17 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +eval feature c -> 0 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +eval feature c -> 1 +stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +choice step 3: writes c := 2 by token 2, c := 1 by token 3 (unordered; c := 2 by token 2 stood) +choice step 3: tokens 2@a, 3@b (unordered; took 3@b first) +step 3: token 2@j, token 3@j +step 4: token 4@done +step 5: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.trace.order b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.trace.order new file mode 100644 index 0000000000..92885dc0e9 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_fork_lost_update.trace.order @@ -0,0 +1,5 @@ +# The fork precedes both branches; the join waits for both. +f < a +f < b +a < j +b < j diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.check.expected.json new file mode 100644 index 0000000000..6570ffbe0f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"c": ["1", "2"]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.declared.trace.golden new file mode 100644 index 0000000000..805c6c3616 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.declared.trace.golden @@ -0,0 +1,17 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a +stmt if + eval feature c -> 0 + eval literal 1 -> 1 + eval operator < -> true + stmt assign c + eval feature c -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +stmt if + eval feature c -> 1 + eval literal 1 -> 1 + eval operator < -> false +choice step 3: tokens 1@a, 2@a (unordered; took 1@a first) +step 3: token 2@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.expected.json new file mode 100644 index 0000000000..39d3a7c861 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.expected.json @@ -0,0 +1,11 @@ +{ + "type": "action", + "evaluate": "test::GuardBranch", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"c": {"type": "Integer", "value": 1}}}, + {"outputs": {"c": {"type": "Integer", "value": 2}}} + ], + "admissible": "A leaf body's start shot and its assignments: another performance may run between them" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.seed-1.trace.golden new file mode 100644 index 0000000000..015d94dfcd --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.seed-1.trace.golden @@ -0,0 +1,24 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a +stmt if + eval feature c -> 0 + eval literal 1 -> 1 + eval operator < -> true +stmt if + eval feature c -> 0 + eval literal 1 -> 1 + eval operator < -> true +choice step 3: tokens 1@a, 2@a (unordered; took 1@a first) +step 3: token 1@a, token 2@a + stmt assign c + eval feature c -> 0 + eval literal 1 -> 1 + eval operator + -> 1 + stmt assign c + eval feature c -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +choice step 4: writes c := 1 by token 1, c := 2 by token 2 (unordered; c := 2 by token 2 stood) +choice step 4: tokens 1@a, 2@a (unordered; took 1@a first) +step 4: token 2@done +step 5: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.sysml b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.sysml new file mode 100644 index 0000000000..65c212055f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + action def GuardBranch { + attribute c : Integer := 0; + // Oracle (docs/project/behavior-semantic-oracle.md): the guard is read before the + // branch's assignment; the other performance may read it too before either writes. + first start then a; + action a[2] { + if c < 1 { + assign c := c + 1; + } + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.trace.golden new file mode 100644 index 0000000000..116af023b6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_guard_branch.trace.golden @@ -0,0 +1,17 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a +stmt if + eval feature c -> 0 + eval literal 1 -> 1 + eval operator < -> true + stmt assign c + eval feature c -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +stmt if + eval feature c -> 1 + eval literal 1 -> 1 + eval operator < -> false +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/action_explore_body_lost_update.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.check.expected.json new file mode 100644 index 0000000000..6570ffbe0f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"c": ["1", "2"]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.declared.trace.golden new file mode 100644 index 0000000000..c71c454693 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.declared.trace.golden @@ -0,0 +1,16 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a +eval feature c -> 0 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +eval feature c -> 1 +stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +choice step 3: writes c := 1 by token 1, c := 2 by token 2 (unordered; c := 2 by token 2 stood) +choice step 3: tokens 1@a, 2@a (unordered; took 1@a first) +step 3: token 2@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.expected.json new file mode 100644 index 0000000000..d3a54f2344 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"c": {"type": "Integer", "value": 1}}}, + {"outputs": {"c": {"type": "Integer", "value": 2}}} + ], + "admissible": "A leaf body's start shot and its assignments: another performance may run between them" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.seed-1.trace.golden new file mode 100644 index 0000000000..1584cc9590 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.seed-1.trace.golden @@ -0,0 +1,18 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a +eval feature c -> 0 +eval feature c -> 0 +choice step 3: tokens 1@a, 2@a (unordered; took 1@a first) +step 3: token 1@a, token 2@a +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +choice step 4: writes c := 1 by token 1, c := 1 by token 2 (unordered; c := 1 by token 2 stood) +choice step 4: tokens 1@a, 2@a (unordered; took 1@a first) +step 4: token 2@done +step 5: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.sysml b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.sysml new file mode 100644 index 0000000000..6299a291ab --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + action def Race { + attribute c : Integer := 0; + first start then a; + // Oracle (docs/project/behavior-semantic-oracle.md): each performance of a + // snapshots c into t at its start shot and assigns t + 1 when its assignment + // ends; the other performance may run between the two, so an update is lost. + action a[2] { + attribute t : Integer := c; + assign c := t + 1; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.trace.golden new file mode 100644 index 0000000000..27e930a878 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_lost_update.trace.golden @@ -0,0 +1,16 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a +eval feature c -> 0 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +eval feature c -> 1 +stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +choice step 3: writes c := 2 by token 1, c := 1 by token 2 (unordered; c := 2 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/action_explore_body_ordered_substeps.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.check.expected.json new file mode 100644 index 0000000000..d903df0d3c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"log": ["\"12b\"", "\"1b2\"", "\"b12\""]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.declared.trace.golden new file mode 100644 index 0000000000..2c34e34536 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.declared.trace.golden @@ -0,0 +1,24 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +enter action node: a +stmt assign log + eval feature log -> "" + eval literal "b" -> "b" + eval operator + -> "b" +choice step 3: tokens 2@a, 3@b (unordered; took 2@a first) +step 3: token 2@start, token 3@j +step 4: token 2@a1, token 3@j +stmt assign log + eval feature log -> "b" + eval literal "1" -> "1" + eval operator + -> "b1" +step 5: token 2@a2, token 3@j +stmt assign log + eval feature log -> "b1" + eval literal "2" -> "2" + eval operator + -> "b12" +step 6: token 2@done, token 3@j +leave action node: a +step 7: token 2@j, token 3@j +step 8: token 4@done +step 9: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.expected.json new file mode 100644 index 0000000000..592cae1594 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.expected.json @@ -0,0 +1,11 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"log": {"type": "String", "value": "12b"}}}, + {"outputs": {"log": {"type": "String", "value": "1b2"}}}, + {"outputs": {"log": {"type": "String", "value": "b12"}}} + ], + "admissible": "A leaf body's start shot and its assignments: another performance may run between them" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.seed-1.trace.golden new file mode 100644 index 0000000000..2c34e34536 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.seed-1.trace.golden @@ -0,0 +1,24 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +enter action node: a +stmt assign log + eval feature log -> "" + eval literal "b" -> "b" + eval operator + -> "b" +choice step 3: tokens 2@a, 3@b (unordered; took 2@a first) +step 3: token 2@start, token 3@j +step 4: token 2@a1, token 3@j +stmt assign log + eval feature log -> "b" + eval literal "1" -> "1" + eval operator + -> "b1" +step 5: token 2@a2, token 3@j +stmt assign log + eval feature log -> "b1" + eval literal "2" -> "2" + eval operator + -> "b12" +step 6: token 2@done, token 3@j +leave action node: a +step 7: token 2@j, token 3@j +step 8: token 4@done +step 9: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.sysml b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.sysml new file mode 100644 index 0000000000..d50fd6a016 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.sysml @@ -0,0 +1,26 @@ +package test { + private import ScalarValues::*; + + action def Ordered { + attribute log : String := ""; + // Oracle (docs/project/behavior-semantic-oracle.md): a's two substeps are + // ordered by a succession, so b may run before, between or after them, never + // with the second before the first. + first start; + then fork f; + then a; + then b; + action a { + first start then a1; + action a1 { assign log := log + "1"; } + then a2; + action a2 { assign log := log + "2"; } + then done; + } + action b { assign log := log + "b"; } + succession a then j; + succession b then j; + join j; + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.trace.golden new file mode 100644 index 0000000000..f0b05dfe14 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.trace.golden @@ -0,0 +1,24 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +stmt assign log + eval feature log -> "" + eval literal "b" -> "b" + eval operator + -> "b" +enter action node: a +choice step 3: tokens 2@a, 3@b (unordered; took 3@b first) +step 3: token 2@start, token 3@j +step 4: token 2@a1, token 3@j +stmt assign log + eval feature log -> "b" + eval literal "1" -> "1" + eval operator + -> "b1" +step 5: token 2@a2, token 3@j +stmt assign log + eval feature log -> "b1" + eval literal "2" -> "2" + eval operator + -> "b12" +step 6: token 2@done, token 3@j +leave action node: a +step 7: token 2@j, token 3@j +step 8: token 4@done +step 9: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.trace.order b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.trace.order new file mode 100644 index 0000000000..58f65d6170 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_ordered_substeps.trace.order @@ -0,0 +1,6 @@ +# The succession orders a1 before a2; the fork precedes both branches. +f < a +f < b +a1 < a2 +a < j +b < j diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_own_callees.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_own_callees.expected.json new file mode 100644 index 0000000000..aa91f866dc --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_own_callees.expected.json @@ -0,0 +1,6 @@ +{ + "type": "action", + "evaluate": "test::OwnCallees", + "libraries": true, + "outputs": {"sum": {"type": "Integer", "value": 5}} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_own_callees.sysml b/internal/exec/runtime/testdata/conformance/action_explore_body_own_callees.sysml new file mode 100644 index 0000000000..9178672f25 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_own_callees.sysml @@ -0,0 +1,31 @@ +package test { + private import ScalarValues::*; + + action def Own { + in x : Integer; + attribute a : Integer := 0; + attribute b : Integer := 0; + first start then s1; + action s1 { assign a := x; } + then s2; + action s2 { assign b := a + 1; } + then done; + } + + action def OwnCallees { + attribute sum : Integer = 0; + // Oracle (docs/project/behavior-semantic-oracle.md): each branch performs Own in an + // executor of its own, touching only attributes and inputs its performance holds; one outcome. + first start; + then fork f; + then p; + then q; + action p : Own { in x = 1; } + action q : Own { in x = 2; } + succession p then j; + succession q then j; + join j; + then action total assign sum := p.b + q.b; + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.check.expected.json new file mode 100644 index 0000000000..87743021f6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"seen": ["1", "2"]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.declared.trace.golden new file mode 100644 index 0000000000..c6e404646c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.declared.trace.golden @@ -0,0 +1,24 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +stmt perform +materialize: counter #1 + eval chain c -> 0 + stmt assign counter.c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +step 1: no active tokens +stmt perform + eval chain c -> 1 + stmt assign counter.c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +step 1: no active tokens +choice step 3: tokens 2@a, 3@b (unordered; took 2@a first) +step 3: token 2@j, token 3@j +step 4: token 4@look +stmt assign seen + eval chain c -> 2 +step 5: token 4@done +step 6: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.expected.json new file mode 100644 index 0000000000..1a09b62933 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.expected.json @@ -0,0 +1,11 @@ +{ + "type": "action", + "evaluate": "test::PerformedCallees", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"seen": {"type": "Integer", "value": 1}}}, + {"outputs": {"seen": {"type": "Integer", "value": 2}}} + ], + "admissible": "A leaf body's start shot and its assignments: another performance may run between them" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.seed-1.trace.golden new file mode 100644 index 0000000000..30309d3281 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.seed-1.trace.golden @@ -0,0 +1,26 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +stmt perform +materialize: counter #1 + eval chain c -> 0 +stmt perform + eval chain c -> 0 +choice step 3: tokens 2@a, 3@b (unordered; took 2@a first) +step 3: token 2@a, token 3@b + stmt assign counter.c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +step 1: no active tokens + stmt assign counter.c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +step 1: no active tokens +choice step 4: tokens 2@a, 3@b (unordered; took 2@a first) +step 4: token 2@j, token 3@j +step 5: token 4@look +stmt assign seen + eval chain c -> 1 +step 6: token 4@done +step 7: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.sysml b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.sysml new file mode 100644 index 0000000000..b3933dd0ac --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.sysml @@ -0,0 +1,34 @@ +package test { + private import ScalarValues::*; + + part def Counter { + attribute c : Integer = 0; + } + part counter : Counter; + + action def Inc { + attribute t : Integer := counter.c; + assign counter.c := t + 1; + } + + action def PerformedCallees { + attribute seen : Integer = 0; + // Oracle (docs/project/behavior-semantic-oracle.md): each branch's body performs + // Inc; either performance may start between the other's snapshot and its write. + first start; + then fork f; + then a; + then b; + action a { + perform action pa : Inc; + } + action b { + perform action pb : Inc; + } + succession a then j; + succession b then j; + join j; + then action look assign seen := counter.c; + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.trace.golden new file mode 100644 index 0000000000..dfb9887740 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_performed_callees.trace.golden @@ -0,0 +1,24 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +stmt perform +materialize: counter #1 + eval chain c -> 0 + stmt assign counter.c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +step 1: no active tokens +stmt perform + eval chain c -> 1 + stmt assign counter.c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +step 1: no active tokens +choice step 3: tokens 2@a, 3@b (unordered; took 3@b first) +step 3: token 2@j, token 3@j +step 4: token 4@look +stmt assign seen + eval chain c -> 2 +step 5: token 4@done +step 6: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.check.expected.json new file mode 100644 index 0000000000..d75f916d89 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"c": ["1", "2", "3"]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.declared.trace.golden new file mode 100644 index 0000000000..3187d937fb --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.declared.trace.golden @@ -0,0 +1,21 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a, token 3@a +eval feature c -> 0 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +eval feature c -> 1 +stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +eval feature c -> 2 +stmt assign c + eval feature t -> 2 + eval literal 1 -> 1 + eval operator + -> 3 +choice step 3: writes c := 1 by token 1, c := 2 by token 2, c := 3 by token 3 (unordered; c := 3 by token 3 stood) +choice step 3: tokens 1@a, 2@a, 3@a (unordered; took 1@a first) +step 3: token 3@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.expected.json new file mode 100644 index 0000000000..f7e2ed4637 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.expected.json @@ -0,0 +1,11 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"c": {"type": "Integer", "value": 1}}}, + {"outputs": {"c": {"type": "Integer", "value": 2}}}, + {"outputs": {"c": {"type": "Integer", "value": 3}}} + ], + "admissible": "A leaf body's start shot and its assignments: another performance may run between them" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.seed-1.trace.golden new file mode 100644 index 0000000000..ed54bd50e2 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.seed-1.trace.golden @@ -0,0 +1,23 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a, token 3@a +eval feature c -> 0 +eval feature c -> 0 +eval feature c -> 0 +choice step 3: tokens 1@a, 2@a, 3@a (unordered; took 3@a first) +step 3: token 1@a, token 2@a, token 3@a +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +choice step 4: writes c := 1 by token 1, c := 1 by token 2, c := 1 by token 3 (unordered; c := 1 by token 2 stood) +choice step 4: tokens 1@a, 2@a, 3@a (unordered; took 3@a first) +step 4: token 2@done +step 5: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.sysml b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.sysml new file mode 100644 index 0000000000..39e96b7802 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.sysml @@ -0,0 +1,15 @@ +package test { + private import ScalarValues::*; + + action def Race3 { + attribute c : Integer := 0; + first start then a; + // Oracle (docs/project/behavior-semantic-oracle.md): three performances of one + // leaf body; how many snapshots precede the last assignment is open. + action a[3] { + attribute t : Integer := c; + assign c := t + 1; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.trace.golden new file mode 100644 index 0000000000..497620f21e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_three_way.trace.golden @@ -0,0 +1,21 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a, token 3@a +eval feature c -> 0 +stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +eval feature c -> 1 +stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +eval feature c -> 2 +stmt assign c + eval feature t -> 2 + eval literal 1 -> 1 + eval operator + -> 3 +choice step 3: writes c := 3 by token 1, c := 2 by token 2, c := 1 by token 3 (unordered; c := 3 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/action_explore_body_typed_callees.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.check.expected.json new file mode 100644 index 0000000000..87743021f6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"seen": ["1", "2"]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.declared.trace.golden new file mode 100644 index 0000000000..a74791f1b9 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.declared.trace.golden @@ -0,0 +1,22 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +materialize: counter #1 +eval chain c -> 0 +stmt assign counter.c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +step 1: no active tokens +eval chain c -> 1 +stmt assign counter.c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +step 1: no active tokens +choice step 3: tokens 2@a, 3@b (unordered; took 2@a first) +step 3: token 2@j, token 3@j +step 4: token 4@look +stmt assign seen + eval chain c -> 2 +step 5: token 4@done +step 6: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.expected.json new file mode 100644 index 0000000000..fcd237f8ad --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.expected.json @@ -0,0 +1,12 @@ +{ + "type": "action", + "evaluate": "test::TypedCallees", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"a.t": {"type": "Integer", "value": 0}, "b.t": {"type": "Integer", "value": 0}, "seen": {"type": "Integer", "value": 1}}}, + {"outputs": {"a.t": {"type": "Integer", "value": 0}, "b.t": {"type": "Integer", "value": 1}, "seen": {"type": "Integer", "value": 2}}}, + {"outputs": {"a.t": {"type": "Integer", "value": 1}, "b.t": {"type": "Integer", "value": 0}, "seen": {"type": "Integer", "value": 2}}} + ], + "admissible": "A leaf body's start shot and its assignments: another performance may run between them" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.seed-1.trace.golden new file mode 100644 index 0000000000..6f5e5bfc81 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.seed-1.trace.golden @@ -0,0 +1,24 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +materialize: counter #1 +eval chain c -> 0 +eval chain c -> 0 +choice step 3: tokens 2@a, 3@b (unordered; took 2@a first) +step 3: token 2@a, token 3@b +stmt assign counter.c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +step 1: no active tokens +stmt assign counter.c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +step 1: no active tokens +choice step 4: tokens 2@a, 3@b (unordered; took 2@a first) +step 4: token 2@j, token 3@j +step 5: token 4@look +stmt assign seen + eval chain c -> 1 +step 6: token 4@done +step 7: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.sysml b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.sysml new file mode 100644 index 0000000000..8c6968aa7e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.sysml @@ -0,0 +1,30 @@ +package test { + private import ScalarValues::*; + + part def Counter { + attribute c : Integer = 0; + } + part counter : Counter; + + action def Inc { + attribute t : Integer := counter.c; + assign counter.c := t + 1; + } + + action def TypedCallees { + attribute seen : Integer = 0; + // Oracle (docs/project/behavior-semantic-oracle.md): each branch performs Inc + // in an executor of its own; either may start between the other's snapshot and its write. + first start; + then fork f; + then a; + then b; + action a : Inc; + action b : Inc; + succession a then j; + succession b then j; + join j; + then action look assign seen := counter.c; + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.trace.golden new file mode 100644 index 0000000000..ce7712a7e6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_body_typed_callees.trace.golden @@ -0,0 +1,22 @@ +step 1: token 1@f +step 2: token 2@a, token 3@b +materialize: counter #1 +eval chain c -> 0 +stmt assign counter.c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +step 1: no active tokens +eval chain c -> 1 +stmt assign counter.c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +step 1: no active tokens +choice step 3: tokens 2@a, 3@b (unordered; took 3@b first) +step 3: token 2@j, token 3@j +step 4: token 4@look +stmt assign seen + eval chain c -> 2 +step 5: token 4@done +step 6: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.check.expected.json new file mode 100644 index 0000000000..f5e425f3ff --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"doubled": ["2.0", "4.0"]}, + "agreed": {"v": "2.0"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.declared.trace.golden new file mode 100644 index 0000000000..ca66017905 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.declared.trace.golden @@ -0,0 +1,15 @@ +step 1: token 1@s +stmt assign v + eval literal 2.0 -> 2.0 +stmt assign doubled + enter calc test::CalcOrder::s::t + eval feature v -> 2.0 + bind k = 2.0 [default] + exit calc test::CalcOrder::s::t + eval feature k -> 2.0 + eval literal 2.0 -> 2.0 + eval operator * -> 4.0 + output test::CalcOrder::s::t.d = 4.0 + eval chain d -> 4.0 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.expected.json new file mode 100644 index 0000000000..c6d5cf42d0 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"v": {"type": "Real", "value": 2.0}, "doubled": {"type": "Real", "value": 2.0}}}, + {"outputs": {"v": {"type": "Real", "value": 2.0}, "doubled": {"type": "Real", "value": 4.0}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.seed-1.trace.golden new file mode 100644 index 0000000000..c692b9d348 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.seed-1.trace.golden @@ -0,0 +1,16 @@ +step 1: token 1@s +choice statements in action node s: next 1 assign v, 2 assign doubled (unordered; took 2 assign doubled first) +stmt assign doubled + enter calc test::CalcOrder::s::t + eval feature v -> 1.0 + bind k = 1.0 [default] + exit calc test::CalcOrder::s::t + eval feature k -> 1.0 + eval literal 2.0 -> 2.0 + eval operator * -> 2.0 + output test::CalcOrder::s::t.d = 2.0 + eval chain d -> 2.0 +stmt assign v + eval literal 2.0 -> 2.0 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.sysml b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.sysml new file mode 100644 index 0000000000..687e654681 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.sysml @@ -0,0 +1,22 @@ +package test { + private import ScalarValues::*; + + calc def Twice { + in k : Real; + out d = k * 2.0; + } + + action def CalcOrder { + attribute v : Real := 1.0; + attribute doubled : Real := 0.0; + first start then s; + // Oracle (docs/project/behavior-semantic-oracle.md): the assignment reading the calc + // usage and the one writing its input are unordered, so `t.d` may bind `v` before it is written. + action s { + assign v := 2.0; + calc t : Twice { in k = v; } + assign doubled := t.d; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.trace.golden new file mode 100644 index 0000000000..ca66017905 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_calc.trace.golden @@ -0,0 +1,15 @@ +step 1: token 1@s +stmt assign v + eval literal 2.0 -> 2.0 +stmt assign doubled + enter calc test::CalcOrder::s::t + eval feature v -> 2.0 + bind k = 2.0 [default] + exit calc test::CalcOrder::s::t + eval feature k -> 2.0 + eval literal 2.0 -> 2.0 + eval operator * -> 4.0 + output test::CalcOrder::s::t.d = 4.0 + eval chain d -> 4.0 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.check.expected.json new file mode 100644 index 0000000000..b3ddc1ad28 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"x": ["10", "6", "7", "9"]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.declared.trace.golden new file mode 100644 index 0000000000..8146d0e091 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.declared.trace.golden @@ -0,0 +1,15 @@ +step 1: token 1@s +stmt assign x + eval feature x -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +stmt assign x + eval feature x -> 2 + eval literal 2 -> 2 + eval operator * -> 4 +stmt assign x + eval feature x -> 4 + eval literal 3 -> 3 + eval operator + -> 7 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.expected.json new file mode 100644 index 0000000000..fd309400d6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.expected.json @@ -0,0 +1,12 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 6}}}, + {"outputs": {"x": {"type": "Integer", "value": 7}}}, + {"outputs": {"x": {"type": "Integer", "value": 9}}}, + {"outputs": {"x": {"type": "Integer", "value": 10}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.seed-1.trace.golden new file mode 100644 index 0000000000..6a4d40bf9d --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.seed-1.trace.golden @@ -0,0 +1,17 @@ +step 1: token 1@s +choice statements in action node s: next 1 assign x, 2 assign x, 3 assign x (unordered; took 2 assign x first) +stmt assign x + eval feature x -> 1 + eval literal 2 -> 2 + eval operator * -> 2 +choice statements in action node s: next 1 assign x, 3 assign x (unordered; took 1 assign x first) +stmt assign x + eval feature x -> 2 + eval literal 1 -> 1 + eval operator + -> 3 +stmt assign x + eval feature x -> 3 + eval literal 3 -> 3 + eval operator + -> 6 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.sysml b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.sysml new file mode 100644 index 0000000000..e617e34e1e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + action def Chain { + attribute x : Integer := 1; + first start then s; + // Oracle (docs/project/behavior-semantic-oracle.md): three assignments of `x` no + // succession orders: six orders, four values. + action s { + assign x := x + 1; + assign x := x * 2; + assign x := x + 3; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.trace.golden new file mode 100644 index 0000000000..8146d0e091 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_chain.trace.golden @@ -0,0 +1,15 @@ +step 1: token 1@s +stmt assign x + eval feature x -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +stmt assign x + eval feature x -> 2 + eval literal 2 -> 2 + eval operator * -> 4 +stmt assign x + eval feature x -> 4 + eval literal 3 -> 3 + eval operator + -> 7 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.check.expected.json new file mode 100644 index 0000000000..bda1e582fd --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"y": ["0", "1"]}, + "agreed": {"x": "1"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.declared.trace.golden new file mode 100644 index 0000000000..b352656ed4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.declared.trace.golden @@ -0,0 +1,7 @@ +step 1: token 1@s +stmt assign x + eval literal 1 -> 1 +stmt assign y + eval feature x -> 1 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.expected.json new file mode 100644 index 0000000000..3e5e02210a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}}}, + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 1}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.seed-1.trace.golden new file mode 100644 index 0000000000..8fa8853545 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.seed-1.trace.golden @@ -0,0 +1,8 @@ +step 1: token 1@s +choice statements in action node s: next 1 assign x, 2 assign y (unordered; took 2 assign y first) +stmt assign y + eval feature x -> 0 +stmt assign x + eval literal 1 -> 1 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.sysml b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.sysml new file mode 100644 index 0000000000..be0dc5cd1b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + action def Order { + attribute x : Integer := 0; + attribute y : Integer := 0; + first start then s; + // Oracle (docs/project/behavior-semantic-oracle.md): the two assignments are + // subactions of `s` no succession orders, so `y` may read `x` before or after it is written. + action s { + assign x := 1; + assign y := x; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.trace.golden new file mode 100644 index 0000000000..b352656ed4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_dependent.trace.golden @@ -0,0 +1,7 @@ +step 1: token 1@s +stmt assign x + eval literal 1 -> 1 +stmt assign y + eval feature x -> 1 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.check.expected.json new file mode 100644 index 0000000000..c58ef9b117 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"y": ["0", "10"]}, + "agreed": {"x": "1"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.declared.trace.golden new file mode 100644 index 0000000000..94722db95f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.declared.trace.golden @@ -0,0 +1,11 @@ +step 1: token 1@s +stmt if + eval feature x -> 0 + eval literal 0 -> 0 + eval operator == -> true + stmt assign y + eval literal 10 -> 10 +stmt assign x + eval literal 1 -> 1 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.expected.json new file mode 100644 index 0000000000..eb36d0d73a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}}}, + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 10}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.seed-1.trace.golden new file mode 100644 index 0000000000..e56586218e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.seed-1.trace.golden @@ -0,0 +1,10 @@ +step 1: token 1@s +choice statements in action node s: next 1 if, 2 assign x (unordered; took 2 assign x first) +stmt assign x + eval literal 1 -> 1 +stmt if + eval feature x -> 1 + eval literal 0 -> 0 + eval operator == -> false +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.sysml b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.sysml new file mode 100644 index 0000000000..b302d41946 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.sysml @@ -0,0 +1,18 @@ +package test { + private import ScalarValues::*; + + action def IfOrder { + attribute x : Integer := 0; + attribute y : Integer := 0; + first start then s; + // Oracle (docs/project/behavior-semantic-oracle.md): the `if` and the assignment after + // it are unordered subactions, but the `if`'s guard precedes its branch. + action s { + if x == 0 { + assign y := 10; + } + assign x := 1; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.trace.golden new file mode 100644 index 0000000000..94722db95f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_if.trace.golden @@ -0,0 +1,11 @@ +step 1: token 1@s +stmt if + eval feature x -> 0 + eval literal 0 -> 0 + eval operator == -> true + stmt assign y + eval literal 10 -> 10 +stmt assign x + eval literal 1 -> 1 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.expected.json new file mode 100644 index 0000000000..d42a9f0e58 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outputs": { + "x": {"type": "Integer", "value": 1}, + "y": {"type": "Integer", "value": 2}, + "z": {"type": "Integer", "value": 3} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.sysml b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.sysml new file mode 100644 index 0000000000..16acfe2548 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.sysml @@ -0,0 +1,18 @@ +package test { + private import ScalarValues::*; + + action def Independent { + attribute x : Integer := 0; + attribute y : Integer := 0; + attribute z : Integer := 0; + first start then s; + // Oracle (docs/project/behavior-semantic-oracle.md): no succession orders the three + // assignments, but none reads or writes what another does, so every order agrees. + action s { + assign x := 1; + assign y := 2; + assign z := 3; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.trace.golden new file mode 100644 index 0000000000..eb7ba64c39 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_independent.trace.golden @@ -0,0 +1,9 @@ +step 1: token 1@s +stmt assign x + eval literal 1 -> 1 +stmt assign y + eval literal 2 -> 2 +stmt assign z + eval literal 3 -> 3 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.check.expected.json new file mode 100644 index 0000000000..bda1e582fd --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"y": ["0", "1"]}, + "agreed": {"x": "1"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.declared.trace.golden new file mode 100644 index 0000000000..041f38756b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.declared.trace.golden @@ -0,0 +1,7 @@ +step 1: token 1@s +stmt assign y + eval feature x -> 0 +stmt assign x + eval literal 1 -> 1 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.expected.json new file mode 100644 index 0000000000..3e5e02210a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}}}, + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 1}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.seed-1.trace.golden new file mode 100644 index 0000000000..84d1936771 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.seed-1.trace.golden @@ -0,0 +1,8 @@ +step 1: token 1@s +choice statements in action node s: next 1 assign y, 2 assign x (unordered; took 2 assign x first) +stmt assign x + eval literal 1 -> 1 +stmt assign y + eval feature x -> 1 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.sysml b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.sysml new file mode 100644 index 0000000000..ce34bab6b2 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + action def ReadFirst { + attribute x : Integer := 0; + attribute y : Integer := 0; + first start then s; + // Oracle (docs/project/behavior-semantic-oracle.md): the read is declared before the + // write, but no succession orders them, so `y` may see `x` written. + action s { + assign y := x; + assign x := 1; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.trace.golden new file mode 100644 index 0000000000..041f38756b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_read_first.trace.golden @@ -0,0 +1,7 @@ +step 1: token 1@s +stmt assign y + eval feature x -> 0 +stmt assign x + eval literal 1 -> 1 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.check.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.check.expected.json new file mode 100644 index 0000000000..606c727a20 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"x": ["0", "1"]}, + "agreed": {"z": "3"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.declared.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.declared.trace.golden new file mode 100644 index 0000000000..1bc7c54174 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.declared.trace.golden @@ -0,0 +1,10 @@ +step 1: token 1@s +stmt assign x + eval literal 1 -> 1 +stmt terminate +terminate action node s: no token dropped +step 2: token 1@next +stmt assign z + eval literal 3 -> 3 +step 3: token 1@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.expected.json new file mode 100644 index 0000000000..53fd22a3e8 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 0}, "z": {"type": "Integer", "value": 3}}}, + {"outputs": {"x": {"type": "Integer", "value": 1}, "z": {"type": "Integer", "value": 3}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.seed-1.trace.golden new file mode 100644 index 0000000000..b3a8459058 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.seed-1.trace.golden @@ -0,0 +1,9 @@ +step 1: token 1@s +choice statements in action node s: next 1 assign x, 2 terminate (unordered; took 2 terminate first) +stmt terminate +terminate action node s: no token dropped +step 2: token 1@next +stmt assign z + eval literal 3 -> 3 +step 3: token 1@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.sysml b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.sysml new file mode 100644 index 0000000000..d0f9b21cf1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.sysml @@ -0,0 +1,17 @@ +package test { + private import ScalarValues::*; + + action def TerminateOrder { + attribute x : Integer := 0; + attribute z : Integer := 0; + first start then s; + // Oracle (docs/project/behavior-semantic-oracle.md): `terminate;` is a subaction no + // succession orders after the assignment, so it may end `s` before `x` is written. + action s { + assign x := 1; + terminate; + } + then action next { assign z := 3; } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.trace.golden new file mode 100644 index 0000000000..1bc7c54174 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_terminate.trace.golden @@ -0,0 +1,10 @@ +step 1: token 1@s +stmt assign x + eval literal 1 -> 1 +stmt terminate +terminate action node s: no token dropped +step 2: token 1@next +stmt assign z + eval literal 3 -> 3 +step 3: token 1@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.expected.json b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.expected.json new file mode 100644 index 0000000000..46ad336eff --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outputs": { + "x": {"type": "Integer", "value": 1}, + "y": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.sysml b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.sysml new file mode 100644 index 0000000000..e051aa572a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + action def Then { + attribute x : Integer := 0; + attribute y : Integer := 0; + first start then s; + // Oracle (docs/project/behavior-semantic-oracle.md): `then` is a succession between + // the two assignments, so `y` reads `x` only after it is written. + action s { + assign x := 1; + then assign y := x; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.trace.golden b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.trace.golden new file mode 100644 index 0000000000..38503fd71e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_explore_statement_order_then.trace.golden @@ -0,0 +1,11 @@ +step 1: token 1@s +enter action node: s +step 2: token 1@assign x +stmt assign x + eval literal 1 -> 1 +step 3: token 1@assign y +stmt assign y + eval feature x -> 1 +leave action node: s +step 4: token 1@done +step 5: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_flow_streaming_from_joined_performance.sysml b/internal/exec/runtime/testdata/conformance/action_flow_streaming_from_joined_performance.sysml index c1c3780819..4d8900dfa0 100644 --- a/internal/exec/runtime/testdata/conformance/action_flow_streaming_from_joined_performance.sysml +++ b/internal/exec/runtime/testdata/conformance/action_flow_streaming_from_joined_performance.sysml @@ -8,7 +8,7 @@ package test { out value : Integer; attribute i : Integer = 0; - action emit { assign i := i + 1; assign value := i; } + action emit assign i := i + 1; then assign value := i; decide again; succession first start then emit; diff --git a/internal/exec/runtime/testdata/conformance/action_flow_streaming_from_performed_action.sysml b/internal/exec/runtime/testdata/conformance/action_flow_streaming_from_performed_action.sysml index a58a9d9e8e..6415e3ec3a 100644 --- a/internal/exec/runtime/testdata/conformance/action_flow_streaming_from_performed_action.sysml +++ b/internal/exec/runtime/testdata/conformance/action_flow_streaming_from_performed_action.sysml @@ -5,7 +5,7 @@ package test { out value : Integer; attribute i : Integer = 0; - action emit { assign i := i + 1; assign value := i; } + action emit assign i := i + 1; then assign value := i; decide again; succession first start then emit; diff --git a/internal/exec/runtime/testdata/conformance/action_flow_streaming_producer_consumer.expected.json b/internal/exec/runtime/testdata/conformance/action_flow_streaming_producer_consumer.expected.json index 300aedb70d..b843190778 100644 --- a/internal/exec/runtime/testdata/conformance/action_flow_streaming_producer_consumer.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_flow_streaming_producer_consumer.expected.json @@ -1,6 +1,7 @@ { "type": "action", "trace": true, + "schedule": "reverse", "outputs": { "total": {"type": "Integer", "value": 6}, "i": {"type": "Integer", "value": 3}, diff --git a/internal/exec/runtime/testdata/conformance/action_flow_succession_producer_consumer.sysml b/internal/exec/runtime/testdata/conformance/action_flow_succession_producer_consumer.sysml index 26c0dc5c7a..ae4744f092 100644 --- a/internal/exec/runtime/testdata/conformance/action_flow_succession_producer_consumer.sysml +++ b/internal/exec/runtime/testdata/conformance/action_flow_succession_producer_consumer.sysml @@ -11,7 +11,7 @@ package test { action producer { out value : Integer; - action emit { assign i := i + 1; assign value := i; } + action emit assign i := i + 1; then assign value := i; decide again; succession first start then emit; diff --git a/internal/exec/runtime/testdata/conformance/action_guard_reads_calc_usage.sysml b/internal/exec/runtime/testdata/conformance/action_guard_reads_calc_usage.sysml index b196e6c05a..45c4f56663 100644 --- a/internal/exec/runtime/testdata/conformance/action_guard_reads_calc_usage.sysml +++ b/internal/exec/runtime/testdata/conformance/action_guard_reads_calc_usage.sysml @@ -14,7 +14,7 @@ package test { first start; action bump { assign i := i + 1; - assign log := log + t.d; + then assign log := log + t.d; } decide check; done; diff --git a/internal/exec/runtime/testdata/conformance/action_guard_statement_order.check.expected.json b/internal/exec/runtime/testdata/conformance/action_guard_statement_order.check.expected.json new file mode 100644 index 0000000000..9664afec98 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_guard_statement_order.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"r": ["1", "2"]} +} diff --git a/internal/exec/runtime/testdata/conformance/action_guard_statement_order.expected.json b/internal/exec/runtime/testdata/conformance/action_guard_statement_order.expected.json new file mode 100644 index 0000000000..f3ffea21de --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_guard_statement_order.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "evaluate": "test::DecisionGuard", + "outcomes": [ + {"outputs": {"r": {"type": "Integer", "value": 1}}}, + {"outputs": {"r": {"type": "Integer", "value": 2}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/action_guard_statement_order.sysml b/internal/exec/runtime/testdata/conformance/action_guard_statement_order.sysml new file mode 100644 index 0000000000..75950dee38 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_guard_statement_order.sysml @@ -0,0 +1,22 @@ +package test { + private import ScalarValues::*; + + constraint def Ok { + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y == 12 + } + + action def DecisionGuard { + out attribute r : Integer := 0; + first start then choose; + decide choose; + if Ok() then yes; + else no; + action yes { assign r := 1; } + then done; + action no { assign r := 2; } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_if_no_else.sysml b/internal/exec/runtime/testdata/conformance/action_if_no_else.sysml index c7268d6740..2e00dc617e 100644 --- a/internal/exec/runtime/testdata/conformance/action_if_no_else.sysml +++ b/internal/exec/runtime/testdata/conformance/action_if_no_else.sysml @@ -7,12 +7,10 @@ package test { attribute total : Integer = 0; first start; - action choose { - if total > 0 { - assign total := total + 10; - } - assign total := total + 1; + action choose if total > 0 { + assign total := total + 10; } + then assign total := total + 1; done; diff --git a/internal/exec/runtime/testdata/conformance/action_nested_leaf_body_preserved.sysml b/internal/exec/runtime/testdata/conformance/action_nested_leaf_body_preserved.sysml index 92a9128df4..96b7662a16 100644 --- a/internal/exec/runtime/testdata/conformance/action_nested_leaf_body_preserved.sysml +++ b/internal/exec/runtime/testdata/conformance/action_nested_leaf_body_preserved.sysml @@ -6,10 +6,8 @@ package test { out attribute legs : Integer; first leg; - action leg { - assign legs := 1; - assign legs := legs + 1; - } + action leg assign legs := 1; + then assign legs := legs + 1; succession first leg then done; done; } diff --git a/internal/exec/runtime/testdata/conformance/action_nested_loop_if.sysml b/internal/exec/runtime/testdata/conformance/action_nested_loop_if.sysml index 11fb0fa9e5..542ea457cc 100644 --- a/internal/exec/runtime/testdata/conformance/action_nested_loop_if.sysml +++ b/internal/exec/runtime/testdata/conformance/action_nested_loop_if.sysml @@ -12,7 +12,7 @@ package test { action accumulate { while count < 6 { assign count := count + 1; - if count == 3 { + then if count == 3 { assign bursts := bursts + 1; attribute inner : Integer = 0; while inner < 3 { diff --git a/internal/exec/runtime/testdata/conformance/action_nested_loop_if.trace.golden b/internal/exec/runtime/testdata/conformance/action_nested_loop_if.trace.golden index 38dfb78c64..ce217d2f9d 100644 --- a/internal/exec/runtime/testdata/conformance/action_nested_loop_if.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_nested_loop_if.trace.golden @@ -4,6 +4,7 @@ stmt while eval feature count -> 0 eval literal 6 -> 6 eval operator < -> true +enter action node: loop body of action node accumulate stmt assign count eval feature count -> 0 eval literal 1 -> 1 @@ -16,10 +17,12 @@ stmt while eval feature total -> 0 eval literal 1 -> 1 eval operator + -> 1 +leave action node: loop body of action node accumulate iteration 2 eval feature count -> 1 eval literal 6 -> 6 eval operator < -> true +enter action node: loop body of action node accumulate stmt assign count eval feature count -> 1 eval literal 1 -> 1 @@ -32,10 +35,12 @@ stmt while eval feature total -> 1 eval literal 1 -> 1 eval operator + -> 2 +leave action node: loop body of action node accumulate iteration 3 eval feature count -> 2 eval literal 6 -> 6 eval operator < -> true +enter action node: loop body of action node accumulate stmt assign count eval feature count -> 2 eval literal 1 -> 1 @@ -91,10 +96,12 @@ stmt while eval feature inner -> 3 eval literal 3 -> 3 eval operator < -> false +leave action node: loop body of action node accumulate iteration 4 eval feature count -> 3 eval literal 6 -> 6 eval operator < -> true +enter action node: loop body of action node accumulate stmt assign count eval feature count -> 3 eval literal 1 -> 1 @@ -107,10 +114,12 @@ stmt while eval feature total -> 5 eval literal 1 -> 1 eval operator + -> 6 +leave action node: loop body of action node accumulate iteration 5 eval feature count -> 4 eval literal 6 -> 6 eval operator < -> true +enter action node: loop body of action node accumulate stmt assign count eval feature count -> 4 eval literal 1 -> 1 @@ -123,10 +132,12 @@ stmt while eval feature total -> 6 eval literal 1 -> 1 eval operator + -> 7 +leave action node: loop body of action node accumulate iteration 6 eval feature count -> 5 eval literal 6 -> 6 eval operator < -> true +enter action node: loop body of action node accumulate stmt assign count eval feature count -> 5 eval literal 1 -> 1 @@ -139,6 +150,7 @@ stmt while eval feature total -> 7 eval literal 1 -> 1 eval operator + -> 8 +leave action node: loop body of action node accumulate iteration 7 eval feature count -> 6 eval literal 6 -> 6 diff --git a/internal/exec/runtime/testdata/conformance/action_node_body_writes_enclosing.sysml b/internal/exec/runtime/testdata/conformance/action_node_body_writes_enclosing.sysml index 7f4d0a5f9d..9c23bf348b 100644 --- a/internal/exec/runtime/testdata/conformance/action_node_body_writes_enclosing.sysml +++ b/internal/exec/runtime/testdata/conformance/action_node_body_writes_enclosing.sysml @@ -12,13 +12,13 @@ package test { then action p { out v : Integer; assign v := 2; - assign count := count + 1; - assign total := v * 10; + then assign count := count + 1; + then assign total := v * 10; } then action q { out v : Integer; assign v := 5; - assign count := count + 1; + then assign count := count + 1; } then done; } diff --git a/internal/exec/runtime/testdata/conformance/action_node_inherited_parameters.sysml b/internal/exec/runtime/testdata/conformance/action_node_inherited_parameters.sysml index 12d23d7c7b..3839a63753 100644 --- a/internal/exec/runtime/testdata/conformance/action_node_inherited_parameters.sysml +++ b/internal/exec/runtime/testdata/conformance/action_node_inherited_parameters.sysml @@ -15,21 +15,17 @@ package test { action def Double :> Scale { in redefines factor = 2; first start; - then action go { - assign scaled := amount * factor; - assign tally := tally + scaled; - assign result := scaled; - } + then action go assign scaled := amount * factor; + then assign tally := tally + scaled; + then assign result := scaled; then done; } action def Triple :> Scale { first start; - then action go { - assign scaled := amount * factor; - assign tally := tally + scaled; - assign result := scaled + 1; - } + then action go assign scaled := amount * factor; + then assign tally := tally + scaled; + then assign result := scaled + 1; then done; } diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_local_frames.sysml b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_local_frames.sysml index aaf3a8404b..e9fae2d40e 100644 --- a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_local_frames.sysml +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_local_frames.sysml @@ -7,7 +7,7 @@ package test { action a[3] { attribute l : Integer = 0; assign l := l + 1; - assign c := c + l; + then assign c := c + l; } then done; } diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.expected.json b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.expected.json new file mode 100644 index 0000000000..a2588f4a04 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outputs": { + "c": {"type": "Integer", "value": 3} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.sysml b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.sysml new file mode 100644 index 0000000000..601dd52084 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.sysml @@ -0,0 +1,12 @@ +package test { + private import ScalarValues::*; + + action def Single { + attribute c : Integer := 0; + first start then a; + // Oracle (docs/project/behavior-semantic-oracle.md): a body of one assignment + // reads and writes c in one move, so no update is lost. + action a[3] { assign c := c + 1; } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.trace.golden b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.trace.golden new file mode 100644 index 0000000000..d817c65b8b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_single_assignment.trace.golden @@ -0,0 +1,18 @@ +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 1 -> 1 + eval operator + -> 1 +stmt assign c + eval feature c -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +stmt assign c + eval feature c -> 2 + eval literal 1 -> 1 + eval operator + -> 3 +choice step 3: writes c := 3 by token 1, c := 2 by token 2, c := 1 by token 3 (unordered; c := 3 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/action_terminate_from_block_node_flow.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_from_block_node_flow.sysml index 4b3c47ef33..3290120850 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_from_block_node_flow.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_from_block_node_flow.sysml @@ -12,24 +12,22 @@ package test { out attribute skipped : Integer = 0; out attribute after : Integer = 0; - action node { - assign before := 1; - if before == 1 { - action forked { - fork split; - action waiter accept sig : Integer; - action quit { assign inner := 2; terminate node; } - first start; - succession first start then split; - succession first split then waiter; - succession first split then quit; - succession first waiter then done; - succession first quit then done; - } - action later { assign skipped := 3; } + action node assign before := 1; + then if before == 1 { + action forked { + fork split; + action waiter accept sig : Integer; + action quit { assign inner := 2; then terminate node; } + first start; + succession first start then split; + succession first split then waiter; + succession first split then quit; + succession first waiter then done; + succession first quit then done; } - assign skipped := 4; + action later { assign skipped := 3; } } + then assign skipped := 4; action tail { assign after := 5; } succession first start then node; succession first node then tail; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_from_block_node_flow.trace.golden b/internal/exec/runtime/testdata/conformance/action_terminate_from_block_node_flow.trace.golden index 70d2ad85d7..7c44361e9d 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_from_block_node_flow.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_terminate_from_block_node_flow.trace.golden @@ -7,10 +7,11 @@ stmt if eval operator == -> true stmt node forked enter action node: forked +enter action node: quit stmt assign inner eval literal 2 -> 2 stmt terminate -terminate action node node: dropped token 3@waiter, token 4@quit +terminate action node node: dropped token 3@waiter, token 4@terminate node step 2: token 1@tail stmt assign after eval literal 5 -> 5 diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_names_block_node.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_names_block_node.sysml index 50680d2804..1074ea1270 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_names_block_node.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_names_block_node.sysml @@ -16,7 +16,7 @@ package test { action forked { fork split; action waiter accept sig : Integer; - action quit { assign inner := 1; terminate outer; } + action quit { assign inner := 1; then terminate outer; } first start; succession first start then split; succession first split then waiter; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_names_block_node.trace.golden b/internal/exec/runtime/testdata/conformance/action_terminate_names_block_node.trace.golden index e7fe9170d1..680acb2795 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_names_block_node.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_terminate_names_block_node.trace.golden @@ -5,10 +5,11 @@ stmt if stmt action body stmt node forked enter action node: forked +enter action node: quit stmt assign inner eval literal 1 -> 1 stmt terminate -terminate action node outer: dropped token 3@waiter, token 4@quit +terminate action node outer: dropped token 3@waiter, token 4@terminate outer stmt node next stmt assign after eval literal 3 -> 3 diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_names_enclosing_node.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_names_enclosing_node.sysml index d550946910..559966eae5 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_names_enclosing_node.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_names_enclosing_node.sysml @@ -12,7 +12,7 @@ package test { then action outer { action inner { assign x := 1; - terminate outer; + then terminate outer; } action late { assign y := 2; } first inner; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node.sysml index bd885f8e35..f0a491bb26 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node.sysml @@ -10,7 +10,7 @@ package test { first start; then action c1 { assign x := 1; - terminate c1; + then terminate c1; } then action c2 { assign y := 2; } then done; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node_concurrently.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node_concurrently.sysml index 02b2e8d77b..ccf177cd7a 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node_concurrently.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node_concurrently.sysml @@ -15,19 +15,17 @@ package test { fork split; action pre; merge gate; - action slow { - assign n := n + 1; - if n == 1 { - action inner { - first start; - then action nap accept after 10 [s]; - then done; - } - assign late := late + 1; - } else { - terminate slow; - assign late := late + 10; + action slow assign n := n + 1; + then if n == 1 { + action inner { + first start; + then action nap accept after 10 [s]; + then done; } + assign late := late + 1; + } else { + terminate slow; + then assign late := late + 10; } action after { assign y := y + 1; } succession first start then split; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node_concurrently.trace.golden b/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node_concurrently.trace.golden index 7ab8a47aef..6c199d9a68 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node_concurrently.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_terminate_names_own_node_concurrently.trace.golden @@ -24,13 +24,16 @@ stmt if eval feature n -> 2 eval literal 1 -> 1 eval operator == -> false +enter action node: branch body of action node slow stmt terminate terminate action node slow: dropped token 4@nap +terminate branch body of action node slow: dropped token 5@terminate slow terminate action node slow: no token dropped stmt assign y eval feature y -> 0 eval literal 1 -> 1 eval operator + -> 1 +choice step 5: tokens 2@after, 3@slow (unordered; took 3@slow first) step 5: token 2@done, token 3@after stmt assign y eval feature y -> 1 diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_nested_body.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_nested_body.sysml index 92b1e79d5b..0136d1e403 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_nested_body.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_nested_body.sysml @@ -11,8 +11,8 @@ package test { first start; then action c1 { assign x := 1; - terminate; - assign y := 2; + then terminate; + then assign y := 2; } then action after { assign z := 3; } then done; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_this_ends_part.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_this_ends_part.sysml index f075b9b156..3511f3e163 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_this_ends_part.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_this_ends_part.sysml @@ -29,8 +29,8 @@ package test { perform action selfDestruct { action step { assign seen := 1; - terminate this; - assign seen := 2; + then terminate this; + then assign seen := 2; } first step; } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_this_ends_part.trace.golden b/internal/exec/runtime/testdata/conformance/action_terminate_this_ends_part.trace.golden index 50c8476b59..1d2fee0241 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_this_ends_part.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_terminate_this_ends_part.trace.golden @@ -11,11 +11,14 @@ enter action node: state behavior beat stmt assign beats eval literal 1 -> 1 run: performed action selfDestruct of #1 +enter action node: step +step 1: token 1@assign seen stmt assign seen eval literal 1 -> 1 +step 2: token 1@terminate this stmt terminate eval feature this -> instance#1 terminate: Probe #1 terminated with occurrence: life (do behavior abandoned: alive) -terminate action: dropped token 1@step -step 1: no active tokens +terminate action: dropped token 1@terminate this +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.check.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.check.expected.json new file mode 100644 index 0000000000..bf341e9d89 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "violation", + "agreed": {"issued": "100", "result": "42", "x": "1"}, + "failures": ["assignment to result: multiplicity violation: 0 value(s) bound to a feature with multiplicity lower bound 1"] +} diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.expected.json index a03f0b26c2..da719a7529 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.expected.json @@ -2,9 +2,9 @@ "type": "action", "evaluate": "test::stopper", "libraries": true, - "outputs": { - "x": {"type": "Integer", "value": 1}, - "result": {"type": "Integer", "value": 42}, - "issued": {"type": "Integer", "value": 100} - } + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 1}, "result": {"type": "Integer", "value": 42}, "issued": {"type": "Integer", "value": 100}}}, + {"error": "assignment to result: multiplicity violation: 0 value(s) bound to a feature with multiplicity lower bound 1"} + ], + "admissible": "A terminate action usage's body and its implicit terminate: each is performed, in which order is open" } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.sysml index b05a03c7ae..48affea985 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_binds_output_pin.sysml @@ -24,7 +24,7 @@ package test { action stop terminate { out code : Integer; perform action report : Report; - assign code := 42; + then assign code := 42; } then action a2 { assign x := 2; } then done; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.check.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.check.expected.json new file mode 100644 index 0000000000..fb87d6678a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"x": ["1", "2"]}, + "agreed": {"y": "0"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.expected.json index e3a176b488..6b2d4c44e1 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.expected.json @@ -1,8 +1,9 @@ { "type": "action", "libraries": true, - "outputs": { - "x": {"type": "Integer", "value": 2}, - "y": {"type": "Integer", "value": 0} - } + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}}}, + {"outputs": {"x": {"type": "Integer", "value": 2}, "y": {"type": "Integer", "value": 0}}} + ], + "admissible": "A terminate action usage's body and its implicit terminate: each is performed, in which order is open" } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.sysml index 4f3a34ee88..c5db53fc04 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself.sysml @@ -13,8 +13,8 @@ package test { then stop; action stop terminate { assign x := x + 1; - terminate; - assign y := 5; + then terminate; + then assign y := 5; } then action a2 { assign y := 2; } then done; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.check.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.check.expected.json new file mode 100644 index 0000000000..bf6e6dd785 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "violation", + "agreed": {"other": "0", "result": "42", "x": "1"}, + "failures": ["assignment to result: multiplicity violation: 0 value(s) bound to a feature with multiplicity lower bound 1"] +} diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.expected.json index ba51cc1fd4..2f01d0a864 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.expected.json @@ -2,9 +2,9 @@ "type": "action", "evaluate": "test::stopper", "libraries": true, - "outputs": { - "x": {"type": "Integer", "value": 1}, - "result": {"type": "Integer", "value": 42}, - "other": {"type": "Integer", "value": 0} - } + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 1}, "result": {"type": "Integer", "value": 42}, "other": {"type": "Integer", "value": 0}}}, + {"error": "assignment to result: multiplicity violation: 0 value(s) bound to a feature with multiplicity lower bound 1"} + ], + "admissible": "A terminate action usage's body and its implicit terminate: each is performed, in which order is open" } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.sysml index 7308890473..4151de2379 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_ends_itself_binds_pin.sysml @@ -18,8 +18,8 @@ package test { out code : Integer; out other : Integer; assign code := 42; - terminate; - assign other := 7; + then terminate; + then assign other := 7; } then action a2 { assign x := 2; } then done; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.check.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.check.expected.json new file mode 100644 index 0000000000..bb6c6dd87e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"cleaned": ["0", "5"], "x": ["1", "6"]}, + "agreed": {"y": "0"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.expected.json index 2bb79bb3c5..451271792d 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.expected.json @@ -2,9 +2,10 @@ "type": "action", "evaluate": "test::stopper", "libraries": true, - "outputs": { - "x": {"type": "Integer", "value": 6}, - "y": {"type": "Integer", "value": 0}, - "cleaned": {"type": "Integer", "value": 5} - } + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}, "cleaned": {"type": "Integer", "value": 0}}}, + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}, "cleaned": {"type": "Integer", "value": 5}}}, + {"outputs": {"x": {"type": "Integer", "value": 6}, "y": {"type": "Integer", "value": 0}, "cleaned": {"type": "Integer", "value": 5}}} + ], + "admissible": "A terminate action usage's body and its implicit terminate: each is performed, in which order is open" } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.sysml index a6834f6803..0b5696a5be 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_body_performs_action.sysml @@ -21,7 +21,7 @@ package test { then stop; action stop terminate { perform action cleanup : Cleanup; - assign x := x + cleaned; + then assign x := x + cleaned; } then action a2 { assign y := 2; } then done; diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_binds_pin.check.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_binds_pin.check.expected.json new file mode 100644 index 0000000000..7da0986b44 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_binds_pin.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "violation", + "agreed": {"result": "42", "y": "0", "z": "3"}, + "failures": ["assignment to result: multiplicity violation: 0 value(s) bound to a feature with multiplicity lower bound 1"] +} diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_binds_pin.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_binds_pin.expected.json index 32cc59d5de..f8f1a9b166 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_binds_pin.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_binds_pin.expected.json @@ -1,9 +1,9 @@ { "type": "action", "libraries": true, - "outputs": { - "result": {"type": "Integer", "value": 42}, - "y": {"type": "Integer", "value": 0}, - "z": {"type": "Integer", "value": 3} - } + "outcomes": [ + {"outputs": {"result": {"type": "Integer", "value": 42}, "y": {"type": "Integer", "value": 0}, "z": {"type": "Integer", "value": 3}}}, + {"error": "assignment to result: multiplicity violation: 0 value(s) bound to a feature with multiplicity lower bound 1"} + ], + "admissible": "A terminate action usage's body and its implicit terminate: each is performed, in which order is open" } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.check.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.check.expected.json new file mode 100644 index 0000000000..59fc91c038 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"x": ["0", "1"]}, + "agreed": {"y": "0", "z": "3"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.expected.json index 41172e7be9..ad8358f53d 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.expected.json @@ -1,9 +1,9 @@ { "type": "action", "libraries": true, - "outputs": { - "x": {"type": "Integer", "value": 1}, - "y": {"type": "Integer", "value": 0}, - "z": {"type": "Integer", "value": 3} - } + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 0}, "y": {"type": "Integer", "value": 0}, "z": {"type": "Integer", "value": 3}}}, + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}, "z": {"type": "Integer", "value": 3}}} + ], + "admissible": "A terminate action usage's body and its implicit terminate: each is performed, in which order is open" } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.sysml index 96f12e7f5b..7fef6e9e70 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_in_block_names_itself.sysml @@ -14,8 +14,8 @@ package test { if true { action stop terminate { assign x := 1; - terminate stop; - assign y := 5; + then terminate stop; + then assign y := 5; } action later { assign y := 2; } } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.check.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.check.expected.json new file mode 100644 index 0000000000..13e4e5ff0a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"code": ["0", "42"], "x": ["1", "2"]}, + "agreed": {"y": "0"} +} diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.expected.json b/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.expected.json index adf0b601ff..d6216d398b 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.expected.json @@ -1,9 +1,10 @@ { "type": "action", "libraries": true, - "outputs": { - "x": {"type": "Integer", "value": 2}, - "y": {"type": "Integer", "value": 0}, - "code": {"type": "Integer", "value": 42} - } + "outcomes": [ + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}, "code": {"type": "Integer", "value": 0}}}, + {"outputs": {"x": {"type": "Integer", "value": 1}, "y": {"type": "Integer", "value": 0}, "code": {"type": "Integer", "value": 42}}}, + {"outputs": {"x": {"type": "Integer", "value": 2}, "y": {"type": "Integer", "value": 0}, "code": {"type": "Integer", "value": 42}}} + ], + "admissible": "A terminate action usage's body and its implicit terminate: each is performed, in which order is open" } diff --git a/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.sysml b/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.sysml index 545600096f..e844126baa 100644 --- a/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.sysml +++ b/internal/exec/runtime/testdata/conformance/action_terminate_usage_with_body.sysml @@ -13,7 +13,7 @@ package test { then stop; action stop terminate { assign code := x + 41; - if code == 42 { assign x := x + 1; } + then if code == 42 { assign x := x + 1; } } then action a2 { assign y := 2; } then done; diff --git a/internal/exec/runtime/testdata/conformance/action_unordered_join_once.sysml b/internal/exec/runtime/testdata/conformance/action_unordered_join_once.sysml index 16e07ab97b..eb0c12242b 100644 --- a/internal/exec/runtime/testdata/conformance/action_unordered_join_once.sysml +++ b/internal/exec/runtime/testdata/conformance/action_unordered_join_once.sysml @@ -12,11 +12,9 @@ package test { attribute doneB : Boolean := false; action a { assign total := total + 1; assign doneA := true; } action b { assign total := total + 10; assign doneB := true; } - action c { - assign seen := total; - assign cRuns := cRuns + 1; - assign total := total + 100; - } + action c assign seen := total; + then assign cRuns := cRuns + 1; + then assign total := total + 100; succession first a then c; succession first b then c; } diff --git a/internal/exec/runtime/testdata/conformance/action_unordered_join_once_nested.sysml b/internal/exec/runtime/testdata/conformance/action_unordered_join_once_nested.sysml index 177b906043..c628678678 100644 --- a/internal/exec/runtime/testdata/conformance/action_unordered_join_once_nested.sysml +++ b/internal/exec/runtime/testdata/conformance/action_unordered_join_once_nested.sysml @@ -12,11 +12,9 @@ package test { action inner { action a { assign total := total + 1; assign doneA := true; } action b { assign total := total + 10; assign doneB := true; } - action c { - assign seen := total; - assign cRuns := cRuns + 1; - assign total := total + 100; - } + action c assign seen := total; + then assign cRuns := cRuns + 1; + then assign total := total + 100; succession first a then c; succession first b then c; } diff --git a/internal/exec/runtime/testdata/conformance/analysis_case_step_order_commuting.expected.json b/internal/exec/runtime/testdata/conformance/analysis_case_step_order_commuting.expected.json new file mode 100644 index 0000000000..10b96b4f9f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_case_step_order_commuting.expected.json @@ -0,0 +1,7 @@ +{ + "libraries": true, + "type": "analysis", + "evaluate": "test::Commute", + "schedule": "reverse", + "result": {"type": "Integer", "value": 3} +} diff --git a/internal/exec/runtime/testdata/conformance/analysis_case_step_order_commuting.sysml b/internal/exec/runtime/testdata/conformance/analysis_case_step_order_commuting.sysml new file mode 100644 index 0000000000..8def20f31d --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_case_step_order_commuting.sysml @@ -0,0 +1,12 @@ +package test { + private import ScalarValues::*; + + analysis def Commute { + return : Integer; + attribute x : Integer := 0; + attribute y : Integer := 0; + action s1 { assign x := 1; } + action s2 { assign y := 2; } + x + y + } +} diff --git a/internal/exec/runtime/testdata/conformance/analysis_case_step_order_stated.expected.json b/internal/exec/runtime/testdata/conformance/analysis_case_step_order_stated.expected.json new file mode 100644 index 0000000000..76b817a206 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_case_step_order_stated.expected.json @@ -0,0 +1,7 @@ +{ + "libraries": true, + "type": "analysis", + "evaluate": "test::Ordered", + "schedule": "reverse", + "result": {"type": "Integer", "value": 12} +} diff --git a/internal/exec/runtime/testdata/conformance/analysis_case_step_order_stated.sysml b/internal/exec/runtime/testdata/conformance/analysis_case_step_order_stated.sysml new file mode 100644 index 0000000000..5c054db891 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_case_step_order_stated.sysml @@ -0,0 +1,13 @@ +package test { + private import ScalarValues::*; + + analysis def Ordered { + return : Integer; + attribute x : Integer := 1; + action s1 { assign x := x * 10; } + action s2 { assign x := x + 2; } + first s1; + then s2; + x + } +} diff --git a/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.check.expected.json b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.check.expected.json new file mode 100644 index 0000000000..f8ce6fdc45 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"r": ["12", "30"]} +} diff --git a/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.declared.trace.golden b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.declared.trace.golden new file mode 100644 index 0000000000..7c42b6d0a3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.declared.trace.golden @@ -0,0 +1,21 @@ +stmt assign r + enter analysis test::An + stmt declare x + eval literal 1 -> 1 + stmt action body +enter action node: analysis test::An + stmt assign x + eval feature x -> 1 + eval literal 10 -> 10 + eval operator * -> 10 + stmt assign x + eval feature x -> 10 + eval literal 2 -> 2 + eval operator + -> 12 +choice step 1: tokens 1@s1, 2@s2 (unordered; took 1@s1 first) +leave action node: analysis test::An + stmt return + eval feature x -> 12 + exit analysis test::An -> 12 + eval invoke An -> 12 +step 1: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.expected.json b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.expected.json new file mode 100644 index 0000000000..2c7916fd97 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.expected.json @@ -0,0 +1,11 @@ +{ + "libraries": true, + "type": "action", + "evaluate": "test::Use", + "trace": true, + "outcomes": [ + {"outputs": {"r": {"type": "Integer", "value": 12}}}, + {"outputs": {"r": {"type": "Integer", "value": 30}}} + ], + "admissible": "Subactions no succession orders: each is performed during the owner, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.seed-1.trace.golden new file mode 100644 index 0000000000..7c42b6d0a3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.seed-1.trace.golden @@ -0,0 +1,21 @@ +stmt assign r + enter analysis test::An + stmt declare x + eval literal 1 -> 1 + stmt action body +enter action node: analysis test::An + stmt assign x + eval feature x -> 1 + eval literal 10 -> 10 + eval operator * -> 10 + stmt assign x + eval feature x -> 10 + eval literal 2 -> 2 + eval operator + -> 12 +choice step 1: tokens 1@s1, 2@s2 (unordered; took 1@s1 first) +leave action node: analysis test::An + stmt return + eval feature x -> 12 + exit analysis test::An -> 12 + eval invoke An -> 12 +step 1: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.sysml b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.sysml new file mode 100644 index 0000000000..e609ff8199 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + analysis def An { + return : Integer; + attribute x : Integer := 1; + action s1 { assign x := x * 10; } + action s2 { assign x := x + 2; } + x + } + + action def Use { + out attribute r : Integer := 0; + assign r := An(); + } +} diff --git a/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.trace.golden b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.trace.golden new file mode 100644 index 0000000000..7c42b6d0a3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order.trace.golden @@ -0,0 +1,21 @@ +stmt assign r + enter analysis test::An + stmt declare x + eval literal 1 -> 1 + stmt action body +enter action node: analysis test::An + stmt assign x + eval feature x -> 1 + eval literal 10 -> 10 + eval operator * -> 10 + stmt assign x + eval feature x -> 10 + eval literal 2 -> 2 + eval operator + -> 12 +choice step 1: tokens 1@s1, 2@s2 (unordered; took 1@s1 first) +leave action node: analysis test::An + stmt return + eval feature x -> 12 + exit analysis test::An -> 12 + eval invoke An -> 12 +step 1: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/analysis_explore_step_order_declared.expected.json b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order_declared.expected.json new file mode 100644 index 0000000000..317fe90abd --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order_declared.expected.json @@ -0,0 +1,7 @@ +{ + "libraries": true, + "type": "analysis", + "evaluate": "test::An", + "schedule": "declared", + "result": {"type": "Integer", "value": 12} +} diff --git a/internal/exec/runtime/testdata/conformance/analysis_explore_step_order_declared.sysml b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order_declared.sysml new file mode 100644 index 0000000000..f6c1d53123 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/analysis_explore_step_order_declared.sysml @@ -0,0 +1,11 @@ +package test { + private import ScalarValues::*; + + analysis def An { + return : Integer; + attribute x : Integer := 1; + action s1 { assign x := x * 10; } + action s2 { assign x := x + 2; } + x + } +} diff --git a/internal/exec/runtime/testdata/conformance/assign_write_through_chain_violates_target_type.sysml b/internal/exec/runtime/testdata/conformance/assign_write_through_chain_violates_target_type.sysml index 854d05c211..2408dea8aa 100644 --- a/internal/exec/runtime/testdata/conformance/assign_write_through_chain_violates_target_type.sysml +++ b/internal/exec/runtime/testdata/conformance/assign_write_through_chain_violates_target_type.sysml @@ -17,7 +17,7 @@ package test { state go { entry action set { assign raw := "x"; - assign cell.mark := raw; + then assign cell.mark := raw; } } } diff --git a/internal/exec/runtime/testdata/conformance/assign_write_to_state_attribute_violates_target_type.sysml b/internal/exec/runtime/testdata/conformance/assign_write_to_state_attribute_violates_target_type.sysml index d503a99321..0feb4f6036 100644 --- a/internal/exec/runtime/testdata/conformance/assign_write_to_state_attribute_violates_target_type.sysml +++ b/internal/exec/runtime/testdata/conformance/assign_write_to_state_attribute_violates_target_type.sysml @@ -12,7 +12,7 @@ package test { state go { entry action set { assign raw := "not a number"; - assign reading := raw; + then assign reading := raw; } } } diff --git a/internal/exec/runtime/testdata/conformance/assign_write_violates_target_type.sysml b/internal/exec/runtime/testdata/conformance/assign_write_violates_target_type.sysml index 3d72562edb..9d186fe8c8 100644 --- a/internal/exec/runtime/testdata/conformance/assign_write_violates_target_type.sysml +++ b/internal/exec/runtime/testdata/conformance/assign_write_violates_target_type.sysml @@ -14,7 +14,7 @@ package test { state go { entry action set { assign raw := "not a number"; - assign reading := raw; + then assign reading := raw; } } } diff --git a/internal/exec/runtime/testdata/conformance/calc_block_flow_node_unvalued_pin.sysml b/internal/exec/runtime/testdata/conformance/calc_block_flow_node_unvalued_pin.sysml index 2412363a4c..365d21ed79 100644 --- a/internal/exec/runtime/testdata/conformance/calc_block_flow_node_unvalued_pin.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_block_flow_node_unvalued_pin.sysml @@ -13,8 +13,8 @@ package test { out v : Integer; out w : Integer; assign v := i * 10; - assign w := v + 1; - assign total := total + w; + then assign w := v + 1; + then assign total := total + w; } } return : Integer = v + total; diff --git a/internal/exec/runtime/testdata/conformance/calc_collection_ops_in_while_loop.sysml b/internal/exec/runtime/testdata/conformance/calc_collection_ops_in_while_loop.sysml index 43486dc376..cade46a924 100644 --- a/internal/exec/runtime/testdata/conformance/calc_collection_ops_in_while_loop.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_collection_ops_in_while_loop.sysml @@ -8,7 +8,7 @@ package test { attribute total : Integer = 0; while i <= xs->size() { assign total := total + xs#(i); - assign i := i + 1; + then assign i := i + 1; } return : Integer = total + xs.?{in x; x > 5}->size(); } diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.check.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.check.expected.json new file mode 100644 index 0000000000..5dcb500155 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"r": ["12", "30"]} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.declared.trace.golden b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.declared.trace.golden new file mode 100644 index 0000000000..1398f9916a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.declared.trace.golden @@ -0,0 +1,17 @@ +stmt assign r + enter calc test::Ord + stmt declare y + eval literal 1 -> 1 + stmt assign y + eval feature y -> 1 + eval literal 10 -> 10 + eval operator * -> 10 + stmt assign y + eval feature y -> 10 + eval literal 2 -> 2 + eval operator + -> 12 + stmt return + eval feature y -> 12 + exit calc test::Ord -> 12 + eval invoke Ord -> 12 +step 1: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.expected.json new file mode 100644 index 0000000000..a90cacdff6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "trace": true, + "outcomes": [ + {"outputs": {"r": {"type": "Integer", "value": 12}}}, + {"outputs": {"r": {"type": "Integer", "value": 30}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.seed-1.trace.golden new file mode 100644 index 0000000000..0ce2d40c53 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.seed-1.trace.golden @@ -0,0 +1,18 @@ +stmt assign r +choice result of calc test::Ord under the statement orders of its body: next 12, 30 (unordered; took 30 first) + enter calc test::Ord + stmt declare y + eval literal 1 -> 1 + stmt assign y + eval feature y -> 1 + eval literal 2 -> 2 + eval operator + -> 3 + stmt assign y + eval feature y -> 3 + eval literal 10 -> 10 + eval operator * -> 30 + stmt return + eval feature y -> 30 + exit calc test::Ord -> 30 + eval invoke Ord -> 30 +step 1: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.sysml b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.sysml new file mode 100644 index 0000000000..9b60f6023e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + calc def Ord { + return : Integer; + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y + } + + action def Use { + out attribute r : Integer := 0; + assign r := Ord(); + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.trace.golden b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.trace.golden new file mode 100644 index 0000000000..1398f9916a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order.trace.golden @@ -0,0 +1,17 @@ +stmt assign r + enter calc test::Ord + stmt declare y + eval literal 1 -> 1 + stmt assign y + eval feature y -> 1 + eval literal 10 -> 10 + eval operator * -> 10 + stmt assign y + eval feature y -> 10 + eval literal 2 -> 2 + eval operator + -> 12 + stmt return + eval feature y -> 12 + exit calc test::Ord -> 12 + eval invoke Ord -> 12 +step 1: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.check.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.check.expected.json new file mode 100644 index 0000000000..64e837d5d5 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "no violation, exhaustive", + "agreed": {"r": "5"} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.expected.json new file mode 100644 index 0000000000..3a44a0fd4f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.expected.json @@ -0,0 +1,5 @@ +{ + "type": "action", + "libraries": true, + "outputs": {"r": {"type": "Integer", "value": 5}} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.sysml b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.sysml new file mode 100644 index 0000000000..04b97624dd --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_commuting.sysml @@ -0,0 +1,17 @@ +package test { + private import ScalarValues::*; + + calc def Pair { + return : Integer; + attribute a : Integer := 0; + attribute b : Integer := 0; + assign a := a + 2; + assign b := b + 3; + a + b + } + + action def Use { + out attribute r : Integer := 0; + assign r := Pair(); + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_declared.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_declared.expected.json new file mode 100644 index 0000000000..e1ba375b31 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_declared.expected.json @@ -0,0 +1,6 @@ +{ + "type": "action", + "libraries": true, + "schedule": "declared", + "outputs": {"r": {"type": "Integer", "value": 12}} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_declared.sysml b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_declared.sysml new file mode 100644 index 0000000000..9b60f6023e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_declared.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + calc def Ord { + return : Integer; + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y + } + + action def Use { + out attribute r : Integer := 0; + assign r := Ord(); + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.check.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.check.expected.json new file mode 100644 index 0000000000..5dcb500155 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"r": ["12", "30"]} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.expected.json new file mode 100644 index 0000000000..9ee50cfb47 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "evaluate": "test::Use", + "outcomes": [ + {"outputs": {"r": {"type": "Integer", "value": 12}}}, + {"outputs": {"r": {"type": "Integer", "value": 30}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.sysml b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.sysml new file mode 100644 index 0000000000..9c73ff2433 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_derived.sysml @@ -0,0 +1,15 @@ +package test { + private import ScalarValues::*; + + calc def Ord { + return : Integer; + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y + } + + action def Use { + out attribute r : Integer = Ord(); + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.check.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.check.expected.json new file mode 100644 index 0000000000..1378d49b8b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"r": ["2", "20"]} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.expected.json new file mode 100644 index 0000000000..67b5bae096 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "outcomes": [ + {"outputs": {"r": {"type": "Integer", "value": 2}}}, + {"outputs": {"r": {"type": "Integer", "value": 20}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.sysml b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.sysml new file mode 100644 index 0000000000..1d01f1dc30 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive.sysml @@ -0,0 +1,20 @@ +package test { + private import ScalarValues::*; + + calc def Descend { + in n : Integer; + return : Integer; + attribute y : Integer := 0; + assign y := y * 10; + assign y := y + 2; + if n > 0 { + return : Integer = Descend(n - 1); + } + y + } + + action def Use { + out attribute r : Integer := 0; + assign r := Descend(2); + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.check.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.check.expected.json new file mode 100644 index 0000000000..1378d49b8b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"r": ["2", "20"]} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.expected.json new file mode 100644 index 0000000000..67b5bae096 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "outcomes": [ + {"outputs": {"r": {"type": "Integer", "value": 2}}}, + {"outputs": {"r": {"type": "Integer", "value": 20}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.sysml b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.sysml new file mode 100644 index 0000000000..54f2190226 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_recursive_deep.sysml @@ -0,0 +1,20 @@ +package test { + private import ScalarValues::*; + + calc def Descend { + in n : Integer; + return : Integer; + attribute y : Integer := 0; + assign y := y * 10; + assign y := y + 2; + if n > 0 { + return : Integer = Descend(n - 1); + } + y + } + + action def Use { + out attribute r : Integer := 0; + assign r := Descend(6); + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.check.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.check.expected.json new file mode 100644 index 0000000000..3f76729d57 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "no violation, exhaustive", + "agreed": {"r": "11"} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.expected.json new file mode 100644 index 0000000000..c0914b1f99 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.expected.json @@ -0,0 +1,5 @@ +{ + "type": "action", + "libraries": true, + "outputs": {"r": {"type": "Integer", "value": 11}} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.sysml b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.sysml new file mode 100644 index 0000000000..2d548827e4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + calc def Then { + return : Integer; + attribute y : Integer := 1; + assign y := y * 2 + 1; + then assign y := y * 3 + 2; + y + } + + action def Use { + out attribute r : Integer := 0; + assign r := Then(); + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.check.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.check.expected.json new file mode 100644 index 0000000000..c213bd96e3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"r": ["15", "23", "35"]} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.expected.json b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.expected.json new file mode 100644 index 0000000000..49a5ec0300 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "libraries": true, + "outcomes": [ + {"outputs": {"r": {"type": "Integer", "value": 15}}}, + {"outputs": {"r": {"type": "Integer", "value": 23}}}, + {"outputs": {"r": {"type": "Integer", "value": 35}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.sysml b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.sysml new file mode 100644 index 0000000000..f308ee12b7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_explore_statement_order_then_unordered.sysml @@ -0,0 +1,17 @@ +package test { + private import ScalarValues::*; + + calc def ThenAndOpen { + return : Integer; + attribute y : Integer := 1; + assign y := y * 2 + 1; + then assign y := y * 3 + 2; + assign y := y + 4; + y + } + + action def Use { + out attribute r : Integer := 0; + assign r := ThenAndOpen(); + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_iterative_factorial.sysml b/internal/exec/runtime/testdata/conformance/calc_iterative_factorial.sysml index 1f97679544..018fea5b8d 100644 --- a/internal/exec/runtime/testdata/conformance/calc_iterative_factorial.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_iterative_factorial.sysml @@ -7,7 +7,7 @@ package test { attribute i : Integer = 1; while i <= n { assign acc := acc * i; - assign i := i + 1; + then assign i := i + 1; } return : Integer = acc; } diff --git a/internal/exec/runtime/testdata/conformance/calc_output_assigned_in_body.sysml b/internal/exec/runtime/testdata/conformance/calc_output_assigned_in_body.sysml index ed6a94bb78..8e233665c6 100644 --- a/internal/exec/runtime/testdata/conformance/calc_output_assigned_in_body.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_output_assigned_in_body.sysml @@ -32,10 +32,10 @@ package test { attribute i : Integer = 1; while i <= n { assign s := s + i; - assign i := i + 1; + then assign i := i + 1; } - assign total := s; - if s > 10 { + then assign total := s; + then if s > 10 { assign size := "big"; } else { assign size := "small"; @@ -49,9 +49,9 @@ package test { out total : Integer; attribute i : Integer = 1; assign total := 0; - while i <= n { + then while i <= n { assign total := total + i; - assign i := i + 1; + then assign i := i + 1; } } @@ -60,7 +60,7 @@ package test { inout p : Integer; out q : Integer; assign p := p + 1; - assign q := p * 10; + then assign q := p * 10; } // A body that returns a value of its own leaves an output it assigned alone: the diff --git a/internal/exec/runtime/testdata/conformance/calc_rk4_lunar_descent.sysml b/internal/exec/runtime/testdata/conformance/calc_rk4_lunar_descent.sysml index a5dc974690..cec04a7e05 100644 --- a/internal/exec/runtime/testdata/conformance/calc_rk4_lunar_descent.sysml +++ b/internal/exec/runtime/testdata/conformance/calc_rk4_lunar_descent.sysml @@ -64,10 +64,10 @@ package test { in m = m + dt * k3.dm; } assign x := x + dt / 6.0 * (k1.dx + 2.0 * k2.dx + 2.0 * k3.dx + k4.dx); - assign h := h + dt / 6.0 * (k1.dh + 2.0 * k2.dh + 2.0 * k3.dh + k4.dh); - assign vx := vx + dt / 6.0 * (k1.dvx + 2.0 * k2.dvx + 2.0 * k3.dvx + k4.dvx); - assign vh := vh + dt / 6.0 * (k1.dvh + 2.0 * k2.dvh + 2.0 * k3.dvh + k4.dvh); - assign m := m + dt / 6.0 * (k1.dm + 2.0 * k2.dm + 2.0 * k3.dm + k4.dm); + then assign h := h + dt / 6.0 * (k1.dh + 2.0 * k2.dh + 2.0 * k3.dh + k4.dh); + then assign vx := vx + dt / 6.0 * (k1.dvx + 2.0 * k2.dvx + 2.0 * k3.dvx + k4.dvx); + then assign vh := vh + dt / 6.0 * (k1.dvh + 2.0 * k2.dvh + 2.0 * k3.dvh + k4.dvh); + then assign m := m + dt / 6.0 * (k1.dm + 2.0 * k2.dm + 2.0 * k3.dm + k4.dm); } return : Real = h; diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps.expected.json b/internal/exec/runtime/testdata/conformance/constraint_body_steps.expected.json new file mode 100644 index 0000000000..ff111f30fc --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps.expected.json @@ -0,0 +1,24 @@ +{ + "libraries": true, + "trace": true, + "type": "instance", + "instantiate": "test::Vehicle", + "slots": {"z": {"type": "Real", "value": 1}}, + "constraints": { + "assigned": true, + "branched": true, + "summed": true, + "shadowed": true, + "ordered": true, + "deep": true, + "outerRead": true, + "outerLeaf": true + }, + "validation": { + "verdicts": [ + {"kind": "constraint", "assertion": "assert not constraint", "status": "holds"}, + {"kind": "requirement", "assertion": "requirement safing", "status": "holds"}, + {"kind": "requirement", "assertion": "requirement fitted", "status": "holds"} + ] + } +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps.sysml b/internal/exec/runtime/testdata/conformance/constraint_body_steps.sysml new file mode 100644 index 0000000000..61f3f44ab8 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps.sysml @@ -0,0 +1,47 @@ +// The statements of a constraint body are steps of one Boolean performance, +// run in declaration order before its conditions are evaluated in the state +// they left: locals live in that performance alone, the constraint's own +// parameters are copied into it, and a nested body keeps its own. +package test { + private import ScalarValues::*; + private import SequenceFunctions::*; + + constraint def SumBelow { + in xs : Integer[*]; + attribute total : Integer = 0; + attribute i : Integer = 0; + while i < size(xs) { assign total := total + xs#(i + 1); then assign i := i + 1; } + total > 3 + } + + // A specialization inherits its steps through the member chain. + constraint def Deeper :> SumBelow { } + + requirement def Safing { + require constraint { attribute m : Real = 0; assign m := 9; m > 4 } + assume constraint { attribute b : Boolean = true; assign b := false; not b } + } + + part def Vehicle { + attribute z : Real = 1.0; + constraint assigned { attribute y : Real = 0; assign y := 5; y > 3 } + constraint branched { attribute y : Real = 0; if z > 0 { assign y := 7; } else { assign y := 1; } y > 3 } + constraint summed : SumBelow { in xs = (2, 4); } + // z names a local of the performance, not the part's feature it shadows. + constraint shadowed { attribute z : Real = 0; assign z := 5; z > 3 } + // Declaration order decides: (1 * 10) + 2 == 12, not 1 * 10 + 2's reverse. + constraint ordered { attribute y : Integer = 1; assign y := y * 10; then assign y := y + 2; y == 12 } + constraint deep : Deeper { in xs = (5, 0); } + // A nested body's steps read the outer body's locals but cannot write them. + constraint outerRead { attribute x : Integer = 0; assign x := 5; assert constraint { if x == 5 { } x == 5 } } + constraint outerLeaf { attribute x : Integer = 0; assign x := 5; assert constraint { x == 5 } } + assert not constraint { attribute w : Real = 0; assign w := 5; w > 100 } + requirement safing : Safing; + requirement fitted { + subject v : Vehicle = vehicle; + require constraint { attribute m : Real = 0; assign m := v.z * 2; m > 1 } + } + } + + part vehicle : Vehicle; +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps.trace.golden b/internal/exec/runtime/testdata/conformance/constraint_body_steps.trace.golden new file mode 100644 index 0000000000..c5efd8880f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps.trace.golden @@ -0,0 +1 @@ +materialize: Vehicle #1 diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_chain_rejected.expected.json b/internal/exec/runtime/testdata/conformance/constraint_body_steps_chain_rejected.expected.json new file mode 100644 index 0000000000..dabb5c8687 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_chain_rejected.expected.json @@ -0,0 +1,5 @@ +{ + "type": "constraint", + "evaluate": "test::Rig::writesChain", + "error": "effect outside the constraint performance: constraint body: v.z writes a feature of an object outside the constraint's own performance" +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_chain_rejected.sysml b/internal/exec/runtime/testdata/conformance/constraint_body_steps_chain_rejected.sysml new file mode 100644 index 0000000000..e6d98b0e75 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_chain_rejected.sysml @@ -0,0 +1,10 @@ +// A chained assignment target writes a feature of the object the chain reaches +// — an effect outside the constraint's own performance, refused by a verdict. +package test { + private import ScalarValues::*; + + part def Rig { + attribute z : Real = 1; + constraint writesChain { attribute v : Rig; assign v.z := 5; v.z > 3 } + } +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.expected.json b/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.expected.json new file mode 100644 index 0000000000..5cd69dc499 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.expected.json @@ -0,0 +1,7 @@ +{ + "libraries": true, + "trace": true, + "type": "constraint", + "evaluate": "test::ordered", + "satisfied": true +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.sysml b/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.sysml new file mode 100644 index 0000000000..9f5ae33103 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.sysml @@ -0,0 +1,13 @@ +// The steps of a constraint body run in declaration order before its +// conditions are evaluated in the state they left: (1 * 10) + 2 == 12, which +// no other order of the two assignments answers. +package test { + private import ScalarValues::*; + + constraint ordered { + attribute y : Integer = 1; + assign y := y * 10; + then assign y := y + 2; + y == 12 + } +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.trace.golden b/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.trace.golden new file mode 100644 index 0000000000..5465a24328 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_order.trace.golden @@ -0,0 +1,13 @@ +stmt declare y + eval literal 1 -> 1 +stmt assign y + eval feature y -> 1 + eval literal 10 -> 10 + eval operator * -> 10 +stmt assign y + eval feature y -> 10 + eval literal 2 -> 2 + eval operator + -> 12 + eval feature y -> 12 + eval literal 12 -> 12 +eval operator == -> true diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_outer_write_rejected.expected.json b/internal/exec/runtime/testdata/conformance/constraint_body_steps_outer_write_rejected.expected.json new file mode 100644 index 0000000000..148c6f45ac --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_outer_write_rejected.expected.json @@ -0,0 +1,5 @@ +{ + "type": "constraint", + "evaluate": "test::Rig::writesPart", + "error": "assignment outside the constraint performance: constraint body: the implicit target of an assignment is the constraint's own performance (SysML v2 §7.17.9) and z is not one of its features" +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_outer_write_rejected.sysml b/internal/exec/runtime/testdata/conformance/constraint_body_steps_outer_write_rejected.sysml new file mode 100644 index 0000000000..9b4e14e8e8 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_outer_write_rejected.sysml @@ -0,0 +1,11 @@ +// An untargeted assignment's implicit target is the constraint's own +// performance (SysML v2 §7.17.9): a name it holds no feature for — one of the +// constrained part's here — is refused, and the object left alone. +package test { + private import ScalarValues::*; + + part def Rig { + attribute z : Real = 1; + constraint writesPart { attribute y : Real = 0; assign z := 5; z > 3 } + } +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_send_rejected.expected.json b/internal/exec/runtime/testdata/conformance/constraint_body_steps_send_rejected.expected.json new file mode 100644 index 0000000000..7f665fa78e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_send_rejected.expected.json @@ -0,0 +1,5 @@ +{ + "type": "constraint", + "evaluate": "test::Rig::sends", + "error": "effect outside the constraint performance: constraint body: a send addresses the world the constraint judges" +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_send_rejected.sysml b/internal/exec/runtime/testdata/conformance/constraint_body_steps_send_rejected.sysml new file mode 100644 index 0000000000..3f049e59cc --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_send_rejected.sysml @@ -0,0 +1,10 @@ +// A send addresses the world the constraint judges — an effect outside the +// constraint's own performance, refused by a verdict. +package test { + private import ScalarValues::*; + + part def Rig { + attribute z : Real = 1; + constraint sends { attribute y : Real = 1; send y to z; z > 0 } + } +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_succession_rejected.expected.json b/internal/exec/runtime/testdata/conformance/constraint_body_steps_succession_rejected.expected.json new file mode 100644 index 0000000000..61e83492eb --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_succession_rejected.expected.json @@ -0,0 +1,5 @@ +{ + "type": "constraint", + "evaluate": "test::Rig::flowed", + "error": "statement not executable: constraint body: `first` statement in a body is not executable" +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_steps_succession_rejected.sysml b/internal/exec/runtime/testdata/conformance/constraint_body_steps_succession_rejected.sysml new file mode 100644 index 0000000000..5433b62815 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_steps_succession_rejected.sysml @@ -0,0 +1,11 @@ +// A succession is a stated flow: a verdict orders its steps by declaration +// rather than by the successions stated, so the flow is refused. +package test { + private import ScalarValues::*; + + part def Rig { + attribute z : Real = 1; + action a; action b; + constraint flowed { first a then b; z > 0 } + } +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.check.expected.json b/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.check.expected.json new file mode 100644 index 0000000000..884e4ba442 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"r": ["false", "true"]} +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.expected.json b/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.expected.json new file mode 100644 index 0000000000..4608b4de1b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "outcomes": [ + {"outputs": {"r": {"type": "Boolean", "value": false}}}, + {"outputs": {"r": {"type": "Boolean", "value": true}}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.sysml b/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.sysml new file mode 100644 index 0000000000..a82f9875a4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_explore_statement_order.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + constraint def Ok { + in x : Integer; + attribute y : Integer := x; + assign y := y * 10; + assign y := y + 2; + y == 12 + } + + action def A { + out attribute r : Boolean := false; + assign r := Ok(1); + } +} diff --git a/internal/exec/runtime/testdata/conformance/extent_held_performances.expected.json b/internal/exec/runtime/testdata/conformance/extent_held_performances.expected.json new file mode 100644 index 0000000000..99ce76e3f1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_held_performances.expected.json @@ -0,0 +1,16 @@ +{ + "type": "calc", + "evaluate": "test::extents", + "libraries": true, + "trace": true, + "inputs": [], + "result": {"type": "Sequence", "elements": [ + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true} + ]} +} diff --git a/internal/exec/runtime/testdata/conformance/extent_held_performances.sysml b/internal/exec/runtime/testdata/conformance/extent_held_performances.sysml new file mode 100644 index 0000000000..bed81f67c1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_held_performances.sysml @@ -0,0 +1,51 @@ +// An extent reaches the occurrences an object performs as well as the parts it owns: +// the action, state, connection, interface, allocation and exhibited or performed +// usages it holds are read and collected for `all T` as the parts are. Constraint, +// requirement and calc usages stay out: their read is an evaluation, not a held +// occurrence. +package test { + private import ScalarValues::*; + private import SequenceFunctions::*; + + action def Act; + state def St { entry; then s0; state s0; } + part def Car; + connection def Lnk { end part x : Car; end part y : Car; } + port def Pt; + interface def Ifc { end port x : Pt; end port y : Pt; } + allocation def Aloc { end part x : Car; end part y : Car; } + calc def Chk { return r : Boolean = true; } + constraint def K { true } + requirement def Req; + + part def Host { + part a : Car; + part b : Car; + port pa : Pt; + port pb : Pt; + action act : Act; + state st : St; + connection cn : Lnk connect a to b; + interface ifc : Ifc connect pa to pb; + allocation al : Aloc allocate a to b; + perform action pf : Act; + exhibit state ex : St; + calc cc : Chk; + constraint ck : K; + requirement rq : Req; + } + + part h : Host; + + calc extents { + return : Boolean[*] nonunique = ( + size(all Act) == 2, + size(all St) == 2, + size(all Lnk) == 1, + size(all Ifc) == 1, + size(all Aloc) == 1, + size(all Car) == 2, + size(all Host) == 1 + ); + } +} diff --git a/internal/exec/runtime/testdata/conformance/extent_held_performances.trace.golden b/internal/exec/runtime/testdata/conformance/extent_held_performances.trace.golden new file mode 100644 index 0000000000..8b5b5c130f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_held_performances.trace.golden @@ -0,0 +1,76 @@ +enter calc test::extents + stmt return + enter calc SequenceFunctions::size +materialize: h #1 +start: performed action pf of #1 +materialize: Act #2 +start: exhibited state machine ex of #1 +materialize: St #3 +enter: s0 +run: exhibited state machine ex of #1 +materialize: Act #4 + eval operator all -> (instance#4, instance#2) + bind seq = (instance#4, instance#2) [argument] + exit calc SequenceFunctions::size -> 2 + eval invoke size -> 2 + eval literal 2 -> 2 + eval operator == -> true + enter calc SequenceFunctions::size +materialize: St #5 + eval operator all -> (instance#5, instance#3) + bind seq = (instance#5, instance#3) [argument] + exit calc SequenceFunctions::size -> 2 + eval invoke size -> 2 + eval literal 2 -> 2 + eval operator == -> true + enter calc SequenceFunctions::size +materialize: Lnk #6 +materialize: Car #7 + eval feature a -> instance#7 +materialize: Car #8 + eval feature b -> instance#8 + eval operator all -> (instance#6) + bind seq = (instance#6) [argument] + exit calc SequenceFunctions::size -> 1 + eval invoke size -> 1 + eval literal 1 -> 1 + eval operator == -> true + enter calc SequenceFunctions::size +materialize: Ifc #9 +materialize: Pt #10 + eval feature pa -> instance#10 +materialize: Pt #11 + eval feature pb -> instance#11 + eval operator all -> (instance#9) + bind seq = (instance#9) [argument] + exit calc SequenceFunctions::size -> 1 + eval invoke size -> 1 + eval literal 1 -> 1 + eval operator == -> true + enter calc SequenceFunctions::size +materialize: Aloc #12 + eval feature a -> instance#7 + eval feature b -> instance#8 + eval operator all -> (instance#12) + bind seq = (instance#12) [argument] + exit calc SequenceFunctions::size -> 1 + eval invoke size -> 1 + eval literal 1 -> 1 + eval operator == -> true + enter calc SequenceFunctions::size + eval operator all -> (instance#7, instance#8) + bind seq = (instance#7, instance#8) [argument] + exit calc SequenceFunctions::size -> 2 + eval invoke size -> 2 + eval literal 2 -> 2 + eval operator == -> true + enter calc SequenceFunctions::size + eval operator all -> (instance#1) + bind seq = (instance#1) [argument] + exit calc SequenceFunctions::size -> 1 + eval invoke size -> 1 + eval literal 1 -> 1 + eval operator == -> true + eval sequence of 7 -> (true, true, true, true, true, true, true) +exit calc test::extents -> (true, true, true, true, true, true, true) +enter: s0 diff --git a/internal/exec/runtime/testdata/conformance/extent_namespace_binding.expected.json b/internal/exec/runtime/testdata/conformance/extent_namespace_binding.expected.json new file mode 100644 index 0000000000..9a479a324c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_namespace_binding.expected.json @@ -0,0 +1,19 @@ +{ + "type": "calc", + "evaluate": "test::extents", + "libraries": true, + "trace": true, + "inputs": [], + "result": {"type": "Sequence", "elements": [ + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true} + ]} +} diff --git a/internal/exec/runtime/testdata/conformance/extent_namespace_binding.sysml b/internal/exec/runtime/testdata/conformance/extent_namespace_binding.sysml new file mode 100644 index 0000000000..a0545f37a5 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_namespace_binding.sysml @@ -0,0 +1,43 @@ +// A binding connector owned by a package makes its ends denote the same objects (KerML 1.0 +// §7.4.6.3, §8.4.4.6.2): usages several bindings join — one equivalence class — denote the +// class's one object whichever member is read, `all Car` reaches it once however it is +// named, a valueless usage bound to a feature chain denotes the chain's object, and a +// binding written in another package joins the usages it names there. +package test { + private import ScalarValues::*; + private import SequenceFunctions::*; + + part def Car; + + part a : Car; + part b : Car; + bind a = b; + part e : Car; + bind b = e; + + part c : Car; + part d : Car { part w2 : Car; } + bind c = d.w2; + + part x : Car; + + calc extents { + return : Boolean[*] nonunique = ( + size(all Car) == 4, + a === b, + e === a, + x === other::y, + c === d.w2, + (all Car)#(1) === a, + (all Car)#(1) === e, + (all Car)#(2) === c, + (all Car)#(3) === d, + (all Car)#(4) === x + ); + } +} + +package other { + part y : test::Car; + bind test::x = y; +} diff --git a/internal/exec/runtime/testdata/conformance/extent_namespace_binding.trace.golden b/internal/exec/runtime/testdata/conformance/extent_namespace_binding.trace.golden new file mode 100644 index 0000000000..9e1bda938a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_namespace_binding.trace.golden @@ -0,0 +1,53 @@ +enter calc test::extents + stmt return + enter calc SequenceFunctions::size +materialize: a #1 +materialize: d #2 +materialize: Car #3 + eval chain w2 -> instance#3 +materialize: x #4 + eval operator all -> (instance#1, instance#3, instance#2, instance#4) + bind seq = (instance#1, instance#3, instance#2, instance#4) [argument] + exit calc SequenceFunctions::size -> 4 + eval invoke size -> 4 + eval literal 4 -> 4 + eval operator == -> true + eval feature a -> instance#1 + eval feature b -> instance#1 + eval operator === -> true + eval feature e -> instance#1 + eval feature a -> instance#1 + eval operator === -> true + eval feature x -> instance#4 + eval feature other::y -> instance#4 + eval operator === -> true + eval feature c -> instance#3 + eval chain w2 -> instance#3 + eval operator === -> true + eval operator all -> (instance#1, instance#3, instance#2, instance#4) + eval literal 1 -> 1 + eval index -> instance#1 + eval feature a -> instance#1 + eval operator === -> true + eval operator all -> (instance#1, instance#3, instance#2, instance#4) + eval literal 1 -> 1 + eval index -> instance#1 + eval feature e -> instance#1 + eval operator === -> true + eval operator all -> (instance#1, instance#3, instance#2, instance#4) + eval literal 2 -> 2 + eval index -> instance#3 + eval feature c -> instance#3 + eval operator === -> true + eval operator all -> (instance#1, instance#3, instance#2, instance#4) + eval literal 3 -> 3 + eval index -> instance#2 + eval feature d -> instance#2 + eval operator === -> true + eval operator all -> (instance#1, instance#3, instance#2, instance#4) + eval literal 4 -> 4 + eval index -> instance#4 + eval feature x -> instance#4 + eval operator === -> true + eval sequence of 10 -> (true, true, true, true, true, true, true, true, true, true) +exit calc test::extents -> (true, true, true, true, true, true, true, true, true, true) diff --git a/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.expected.json b/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.expected.json new file mode 100644 index 0000000000..99ce76e3f1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.expected.json @@ -0,0 +1,16 @@ +{ + "type": "calc", + "evaluate": "test::extents", + "libraries": true, + "trace": true, + "inputs": [], + "result": {"type": "Sequence", "elements": [ + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true}, + {"type": "Boolean", "value": true} + ]} +} diff --git a/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.sysml b/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.sysml new file mode 100644 index 0000000000..52c0916c67 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.sysml @@ -0,0 +1,32 @@ +// The objects a namespace-level collection usage denotes include the objects the usages +// subsetting it denote, anonymous members making up only what the lower bound still asks +// for (KerML 1.0 §7.3.4.4): an abstract usage holds its concrete specializations' values +// alone, and a collection of one is the object its subsetter denotes. +package test { + private import ScalarValues::*; + private import SequenceFunctions::*; + + part def Car; + + part vs : Car[2]; + part c1 : Car[1] :> vs; + + abstract part avs : Car[2]; + part c2 : Car[1] :> avs; + part c3 : Car[1] :> avs; + + part v : Car[1]; + part c : Car[1] :> v; + + calc extents { + return : Boolean[*] nonunique = ( + size(all Car) == 5, + size(vs) == 2, + vs#(1) === c1, + size(avs) == 2, + avs#(1) === c2, + avs#(2) === c3, + v === c + ); + } +} diff --git a/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.trace.golden b/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.trace.golden new file mode 100644 index 0000000000..ba1e1ff871 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/extent_namespace_subsetters.trace.golden @@ -0,0 +1,48 @@ +enter calc test::extents + stmt return + enter calc SequenceFunctions::size +materialize: c1 #1 +materialize: vs #2 +materialize: c2 #3 +materialize: c3 #4 +materialize: c #5 + eval operator all -> (instance#1, instance#2, instance#3, instance#4, instance#5) + bind seq = (instance#1, instance#2, instance#3, instance#4, instance#5) [argument] + exit calc SequenceFunctions::size -> 5 + eval invoke size -> 5 + eval literal 5 -> 5 + eval operator == -> true + enter calc SequenceFunctions::size + eval feature vs -> (instance#1, instance#2) + bind seq = (instance#1, instance#2) [argument] + exit calc SequenceFunctions::size -> 2 + eval invoke size -> 2 + eval literal 2 -> 2 + eval operator == -> true + eval feature vs -> (instance#1, instance#2) + eval literal 1 -> 1 + eval index -> instance#1 + eval feature c1 -> instance#1 + eval operator === -> true + enter calc SequenceFunctions::size + eval feature avs -> (instance#3, instance#4) + bind seq = (instance#3, instance#4) [argument] + exit calc SequenceFunctions::size -> 2 + eval invoke size -> 2 + eval literal 2 -> 2 + eval operator == -> true + eval feature avs -> (instance#3, instance#4) + eval literal 1 -> 1 + eval index -> instance#3 + eval feature c2 -> instance#3 + eval operator === -> true + eval feature avs -> (instance#3, instance#4) + eval literal 2 -> 2 + eval index -> instance#4 + eval feature c3 -> instance#4 + eval operator === -> true + eval feature v -> instance#5 + eval feature c -> instance#5 + eval operator === -> true + eval sequence of 7 -> (true, true, true, true, true, true, true) +exit calc test::extents -> (true, true, true, true, true, true, true) diff --git a/internal/exec/runtime/testdata/conformance/object_created_by_constructor.sysml b/internal/exec/runtime/testdata/conformance/object_created_by_constructor.sysml index aad4acf4b8..069917558d 100644 --- a/internal/exec/runtime/testdata/conformance/object_created_by_constructor.sysml +++ b/internal/exec/runtime/testdata/conformance/object_created_by_constructor.sysml @@ -27,10 +27,8 @@ package test { perform action build { first start; - then action make { - assign cars := addNew(cars, new Car(1)); - assign cars := addNew(cars, new Car(2)); - } + then action make assign cars := addNew(cars, new Car(1)); + then assign cars := addNew(cars, new Car(2)); then action measure { assign count := size(cars); assign extent := size(all Car); diff --git a/internal/exec/runtime/testdata/conformance/object_destroyed_at_runtime.sysml b/internal/exec/runtime/testdata/conformance/object_destroyed_at_runtime.sysml index a65564e97c..59c3dea358 100644 --- a/internal/exec/runtime/testdata/conformance/object_destroyed_at_runtime.sysml +++ b/internal/exec/runtime/testdata/conformance/object_destroyed_at_runtime.sysml @@ -26,19 +26,15 @@ package test { perform action cycle { first start; - then action make { - assign cars := addNew(cars, new Car(1)); - assign cars := addNew(cars, new Car(2)); - assign spare := cars#(2); - assign extentBefore := size(all Car); - } - then action scrap { - assign sameObject := destroy(spare) === cars#(2); - assign extentAfter := size(all Car); - assign heldAfter := size(cars); - assign spareAlive := isDuring(spare); - assign firstAlive := isDuring(cars#(1)); - } + then action make assign cars := addNew(cars, new Car(1)); + then assign cars := addNew(cars, new Car(2)); + then assign spare := cars#(2); + then assign extentBefore := size(all Car); + then action scrap assign sameObject := destroy(spare) === cars#(2); + then assign extentAfter := size(all Car); + then assign heldAfter := size(cars); + then assign spareAlive := isDuring(spare); + then assign firstAlive := isDuring(cars#(1)); then done; } } diff --git a/internal/exec/runtime/testdata/conformance/object_flow_merge_per_arrival.sysml b/internal/exec/runtime/testdata/conformance/object_flow_merge_per_arrival.sysml index 43a4c9588b..d4fd81bcfb 100644 --- a/internal/exec/runtime/testdata/conformance/object_flow_merge_per_arrival.sysml +++ b/internal/exec/runtime/testdata/conformance/object_flow_merge_per_arrival.sysml @@ -12,7 +12,7 @@ package test { in ref inputObject2 : Integer; out ref outputObject1 : Integer = (inputObject1, inputObject2); } - action inc { in x : Integer; out y : Integer; assign y := x + 1; assign n := y; } + action inc { in x : Integer; out y : Integer; assign y := x + 1; then assign n := y; } first inc then check; decide check; if n < 3 then again; diff --git a/internal/exec/runtime/testdata/conformance/object_flow_merge_per_arrival.trace.golden b/internal/exec/runtime/testdata/conformance/object_flow_merge_per_arrival.trace.golden index a63034ce98..6b1ff6dcee 100644 --- a/internal/exec/runtime/testdata/conformance/object_flow_merge_per_arrival.trace.golden +++ b/internal/exec/runtime/testdata/conformance/object_flow_merge_per_arrival.trace.golden @@ -6,45 +6,57 @@ step 2: token 1@again eval feature inputObject2 -> () eval sequence of 2 -> (0) step 3: token 1@inc +enter action node: inc +step 4: token 1@assign y stmt assign y eval feature x -> 0 eval literal 1 -> 1 eval operator + -> 1 +step 5: token 1@assign n stmt assign n eval feature y -> 1 -step 4: token 1@check +leave action node: inc +step 6: token 1@check eval feature n -> 1 eval literal 3 -> 3 eval operator < -> true -step 5: token 1@again +step 7: token 1@again eval feature inputObject1 -> () eval feature inputObject2 -> 1 eval sequence of 2 -> (1) -step 6: token 1@inc +step 8: token 1@inc +enter action node: inc +step 9: token 1@assign y stmt assign y eval feature x -> 1 eval literal 1 -> 1 eval operator + -> 2 +step 10: token 1@assign n stmt assign n eval feature y -> 2 -step 7: token 1@check +leave action node: inc +step 11: token 1@check eval feature n -> 2 eval literal 3 -> 3 eval operator < -> true -step 8: token 1@again +step 12: token 1@again eval feature inputObject1 -> () eval feature inputObject2 -> 2 eval sequence of 2 -> (2) -step 9: token 1@inc +step 13: token 1@inc +enter action node: inc +step 14: token 1@assign y stmt assign y eval feature x -> 2 eval literal 1 -> 1 eval operator + -> 3 +step 15: token 1@assign n stmt assign n eval feature y -> 3 -step 10: token 1@check +leave action node: inc +step 16: token 1@check eval feature n -> 3 eval literal 3 -> 3 eval operator < -> false -step 11: token 1@done -step 12: no active tokens +step 17: token 1@done +step 18: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/occurrence_is_during_in_exhibited_state.sysml b/internal/exec/runtime/testdata/conformance/occurrence_is_during_in_exhibited_state.sysml index 4a0af8f6d4..28830debc2 100644 --- a/internal/exec/runtime/testdata/conformance/occurrence_is_during_in_exhibited_state.sysml +++ b/internal/exec/runtime/testdata/conformance/occurrence_is_during_in_exhibited_state.sysml @@ -17,9 +17,9 @@ package test { state driving { entry action mark { assign machineDuring := isDuring(modes); - assign partDuring := isDuring(w); - assign w := destroy(w); - assign partAfter := isDuring(w); + then assign partDuring := isDuring(w); + then assign w := destroy(w); + then assign partAfter := isDuring(w); } } } diff --git a/internal/exec/runtime/testdata/conformance/occurrence_lifecycle_in_performed_action.sysml b/internal/exec/runtime/testdata/conformance/occurrence_lifecycle_in_performed_action.sysml index a00e3caef5..ad30148893 100644 --- a/internal/exec/runtime/testdata/conformance/occurrence_lifecycle_in_performed_action.sysml +++ b/internal/exec/runtime/testdata/conformance/occurrence_lifecycle_in_performed_action.sysml @@ -25,19 +25,15 @@ package test { part fresh : Widget; part spare : Widget; first start; - then action begin { - assign createdIsFresh := create(fresh) === fresh; - assign freshDuringGo := isDuring(fresh); - } - then action work { - assign armDuring := isDuring(arm); - assign goDuring := isDuring(go); - assign freshDuringWork := isDuring(fresh); - assign spares := addNew(spares, spare); - assign spareCount := size(spares); - assign arm := destroy(arm); - assign armAfter := isDuring(arm); - } + then action begin assign createdIsFresh := create(fresh) === fresh; + then assign freshDuringGo := isDuring(fresh); + then action work assign armDuring := isDuring(arm); + then assign goDuring := isDuring(go); + then assign freshDuringWork := isDuring(fresh); + then assign spares := addNew(spares, spare); + then assign spareCount := size(spares); + then assign arm := destroy(arm); + then assign armAfter := isDuring(arm); then done; } } diff --git a/internal/exec/runtime/testdata/conformance/performed_action_def_perform_members_are_steps.sysml b/internal/exec/runtime/testdata/conformance/performed_action_def_perform_members_are_steps.sysml index 9cc0d81706..b638e41cfb 100644 --- a/internal/exec/runtime/testdata/conformance/performed_action_def_perform_members_are_steps.sysml +++ b/internal/exec/runtime/testdata/conformance/performed_action_def_perform_members_are_steps.sysml @@ -17,10 +17,8 @@ package test { action def Sample { in ref context : Station[1]; perform action read ::> context.sensor.sample; - action sum { - assign context.last := context.sensor.reading; - assign context.total := context.total + context.last; - } + action sum assign context.last := context.sensor.reading; + then assign context.total := context.total + context.last; first start then read; first read then sum; } diff --git a/internal/exec/runtime/testdata/conformance/send_new_redefined_payload_feature.sysml b/internal/exec/runtime/testdata/conformance/send_new_redefined_payload_feature.sysml index 63d36edd6e..9433f80d74 100644 --- a/internal/exec/runtime/testdata/conformance/send_new_redefined_payload_feature.sysml +++ b/internal/exec/runtime/testdata/conformance/send_new_redefined_payload_feature.sysml @@ -24,7 +24,7 @@ package test { state sending { entry action downlink { send new Sub(4.0, 6.0) via antenna; - send new Sub(b = 5.0) via antenna; + then send new Sub(b = 5.0) via antenna; } } } diff --git a/internal/exec/runtime/testdata/conformance/state_anonymous_action_body.sysml b/internal/exec/runtime/testdata/conformance/state_anonymous_action_body.sysml index f058def753..03ff23c32d 100644 --- a/internal/exec/runtime/testdata/conformance/state_anonymous_action_body.sysml +++ b/internal/exec/runtime/testdata/conformance/state_anonymous_action_body.sysml @@ -15,7 +15,7 @@ package Test { attribute k : Integer = 3; while k > 0 { assign log := log * 10 + k; - assign k := k - 1; + then assign k := k - 1; } } do action { assign log := log * 10 + 8; } diff --git a/internal/exec/runtime/testdata/conformance/state_anonymous_action_body.trace.golden b/internal/exec/runtime/testdata/conformance/state_anonymous_action_body.trace.golden index 92ad91769c..f8df57842e 100644 --- a/internal/exec/runtime/testdata/conformance/state_anonymous_action_body.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_anonymous_action_body.trace.golden @@ -8,6 +8,7 @@ stmt action body eval feature k -> 3 eval literal 0 -> 0 eval operator > -> true +enter action node: loop body of state behavior stmt assign log eval feature log -> 0 eval literal 10 -> 10 @@ -18,10 +19,12 @@ stmt action body eval feature k -> 3 eval literal 1 -> 1 eval operator - -> 2 +leave action node: loop body of state behavior iteration 2 eval feature k -> 2 eval literal 0 -> 0 eval operator > -> true +enter action node: loop body of state behavior stmt assign log eval feature log -> 3 eval literal 10 -> 10 @@ -32,10 +35,12 @@ stmt action body eval feature k -> 2 eval literal 1 -> 1 eval operator - -> 1 +leave action node: loop body of state behavior iteration 3 eval feature k -> 1 eval literal 0 -> 0 eval operator > -> true +enter action node: loop body of state behavior stmt assign log eval feature log -> 32 eval literal 10 -> 10 @@ -46,6 +51,7 @@ stmt action body eval feature k -> 1 eval literal 1 -> 1 eval operator - -> 0 +leave action node: loop body of state behavior iteration 4 eval feature k -> 0 eval literal 0 -> 0 diff --git a/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.declared.trace.golden index b2c41ce010..ee2faa6d0b 100644 --- a/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.declared.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.declared.trace.golden @@ -3,6 +3,7 @@ enter: lwork transition: lstart -> lwork do: lwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 0 eval literal 10 -> 10 @@ -22,6 +23,7 @@ do: lwork eval operator + -> 12 do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 12 eval literal 10 -> 10 @@ -36,6 +38,7 @@ do: lwork eval operator * -> 1240 eval literal 3 -> 3 eval operator + -> 1243 +leave action node: state behavior do: rwork stmt assign seq eval feature seq -> 1243 @@ -53,6 +56,7 @@ do: rwork eval operator * -> 124350 eval literal 6 -> 6 eval operator + -> 124356 +leave action node: state behavior exit: rwork enter: rdone transition: rwork -> rdone diff --git a/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.seed-1.trace.golden index 9d125119cd..6cef5ba4b8 100644 --- a/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.seed-1.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.seed-1.trace.golden @@ -3,6 +3,7 @@ enter: rwork transition: rstart -> rwork do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 0 eval literal 10 -> 10 @@ -22,6 +23,7 @@ do: rwork eval operator + -> 45 do: lwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 45 eval literal 10 -> 10 @@ -36,6 +38,7 @@ do: rwork eval operator * -> 4510 eval literal 6 -> 6 eval operator + -> 4516 +leave action node: state behavior do: lwork stmt assign seq eval feature seq -> 4516 @@ -53,6 +56,7 @@ do: lwork eval operator * -> 451620 eval literal 3 -> 3 eval operator + -> 451623 +leave action node: state behavior exit: lwork enter: ldone transition: lwork -> ldone diff --git a/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.sysml b/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.sysml index f7399e008e..ac54825362 100644 --- a/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.sysml +++ b/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.sysml @@ -13,8 +13,8 @@ package Test { state lwork { do action { assign seq := seq * 10 + 1; - assign seq := seq * 10 + 2; - assign seq := seq * 10 + 3; + then assign seq := seq * 10 + 2; + then assign seq := seq * 10 + 3; } } state ldone; @@ -29,8 +29,8 @@ package Test { state rwork { do action { assign seq := seq * 10 + 4; - assign seq := seq * 10 + 5; - assign seq := seq * 10 + 6; + then assign seq := seq * 10 + 5; + then assign seq := seq * 10 + 6; } } state rdone; diff --git a/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.trace.golden b/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.trace.golden index b2c41ce010..ee2faa6d0b 100644 --- a/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_anonymous_do_atomic.trace.golden @@ -3,6 +3,7 @@ enter: lwork transition: lstart -> lwork do: lwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 0 eval literal 10 -> 10 @@ -22,6 +23,7 @@ do: lwork eval operator + -> 12 do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 12 eval literal 10 -> 10 @@ -36,6 +38,7 @@ do: lwork eval operator * -> 1240 eval literal 3 -> 3 eval operator + -> 1243 +leave action node: state behavior do: rwork stmt assign seq eval feature seq -> 1243 @@ -53,6 +56,7 @@ do: rwork eval operator * -> 124350 eval literal 6 -> 6 eval operator + -> 124356 +leave action node: state behavior exit: rwork enter: rdone transition: rwork -> rdone diff --git a/internal/exec/runtime/testdata/conformance/state_block_flow_node_unvalued_pin.sysml b/internal/exec/runtime/testdata/conformance/state_block_flow_node_unvalued_pin.sysml index 1117a47d18..0e33700c81 100644 --- a/internal/exec/runtime/testdata/conformance/state_block_flow_node_unvalued_pin.sysml +++ b/internal/exec/runtime/testdata/conformance/state_block_flow_node_unvalued_pin.sysml @@ -18,12 +18,12 @@ package test { action p { out v : Integer; assign v := 1; - assign total := total + v; + then assign total := total + v; } action q { out v : Integer; assign v := 20; - assign total := total + v; + then assign total := total + v; } } } 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 521791e38d..382cca67ce 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 @@ -33,7 +33,7 @@ package test { assign runs := runs + 1; } } - if total > 50 { + then if total > 50 { action doubled = Scale(total, 2) { assign total := y; } diff --git a/internal/exec/runtime/testdata/conformance/state_braced_block_local_attribute.sysml b/internal/exec/runtime/testdata/conformance/state_braced_block_local_attribute.sysml index 32a0bdd4df..b3e143d46a 100644 --- a/internal/exec/runtime/testdata/conformance/state_braced_block_local_attribute.sysml +++ b/internal/exec/runtime/testdata/conformance/state_braced_block_local_attribute.sysml @@ -19,7 +19,7 @@ package Test { attribute k : Integer = 3; while k > 0 { assign log := log * 10 + k; - assign k := k - 1; + then assign k := k - 1; } } do { diff --git a/internal/exec/runtime/testdata/conformance/state_braced_block_local_attribute.trace.golden b/internal/exec/runtime/testdata/conformance/state_braced_block_local_attribute.trace.golden index 6d47d88673..ecec776b71 100644 --- a/internal/exec/runtime/testdata/conformance/state_braced_block_local_attribute.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_braced_block_local_attribute.trace.golden @@ -8,6 +8,7 @@ stmt action body eval feature k -> 3 eval literal 0 -> 0 eval operator > -> true +enter action node: loop body of state behavior stmt assign log eval feature log -> 0 eval literal 10 -> 10 @@ -18,10 +19,12 @@ stmt action body eval feature k -> 3 eval literal 1 -> 1 eval operator - -> 2 +leave action node: loop body of state behavior iteration 2 eval feature k -> 2 eval literal 0 -> 0 eval operator > -> true +enter action node: loop body of state behavior stmt assign log eval feature log -> 3 eval literal 10 -> 10 @@ -32,10 +35,12 @@ stmt action body eval feature k -> 2 eval literal 1 -> 1 eval operator - -> 1 +leave action node: loop body of state behavior iteration 3 eval feature k -> 1 eval literal 0 -> 0 eval operator > -> true +enter action node: loop body of state behavior stmt assign log eval feature log -> 32 eval literal 10 -> 10 @@ -46,6 +51,7 @@ stmt action body eval feature k -> 1 eval literal 1 -> 1 eval operator - -> 0 +leave action node: loop body of state behavior iteration 4 eval feature k -> 0 eval literal 0 -> 0 diff --git a/internal/exec/runtime/testdata/conformance/state_concurrent_do.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_concurrent_do.declared.trace.golden index b2c41ce010..ee2faa6d0b 100644 --- a/internal/exec/runtime/testdata/conformance/state_concurrent_do.declared.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_concurrent_do.declared.trace.golden @@ -3,6 +3,7 @@ enter: lwork transition: lstart -> lwork do: lwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 0 eval literal 10 -> 10 @@ -22,6 +23,7 @@ do: lwork eval operator + -> 12 do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 12 eval literal 10 -> 10 @@ -36,6 +38,7 @@ do: lwork eval operator * -> 1240 eval literal 3 -> 3 eval operator + -> 1243 +leave action node: state behavior do: rwork stmt assign seq eval feature seq -> 1243 @@ -53,6 +56,7 @@ do: rwork eval operator * -> 124350 eval literal 6 -> 6 eval operator + -> 124356 +leave action node: state behavior exit: rwork enter: rdone transition: rwork -> rdone diff --git a/internal/exec/runtime/testdata/conformance/state_concurrent_do.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_concurrent_do.seed-1.trace.golden index 9d125119cd..6cef5ba4b8 100644 --- a/internal/exec/runtime/testdata/conformance/state_concurrent_do.seed-1.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_concurrent_do.seed-1.trace.golden @@ -3,6 +3,7 @@ enter: rwork transition: rstart -> rwork do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 0 eval literal 10 -> 10 @@ -22,6 +23,7 @@ do: rwork eval operator + -> 45 do: lwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 45 eval literal 10 -> 10 @@ -36,6 +38,7 @@ do: rwork eval operator * -> 4510 eval literal 6 -> 6 eval operator + -> 4516 +leave action node: state behavior do: lwork stmt assign seq eval feature seq -> 4516 @@ -53,6 +56,7 @@ do: lwork eval operator * -> 451620 eval literal 3 -> 3 eval operator + -> 451623 +leave action node: state behavior exit: lwork enter: ldone transition: lwork -> ldone diff --git a/internal/exec/runtime/testdata/conformance/state_concurrent_do.sysml b/internal/exec/runtime/testdata/conformance/state_concurrent_do.sysml index f676e09971..c2c9b20bfa 100644 --- a/internal/exec/runtime/testdata/conformance/state_concurrent_do.sysml +++ b/internal/exec/runtime/testdata/conformance/state_concurrent_do.sysml @@ -14,8 +14,8 @@ package Test { state lwork { do { assign seq := seq * 10 + 1; - assign seq := seq * 10 + 2; - assign seq := seq * 10 + 3; + then assign seq := seq * 10 + 2; + then assign seq := seq * 10 + 3; } } state ldone; @@ -30,8 +30,8 @@ package Test { state rwork { do { assign seq := seq * 10 + 4; - assign seq := seq * 10 + 5; - assign seq := seq * 10 + 6; + then assign seq := seq * 10 + 5; + then assign seq := seq * 10 + 6; } } state rdone; diff --git a/internal/exec/runtime/testdata/conformance/state_concurrent_do.trace.golden b/internal/exec/runtime/testdata/conformance/state_concurrent_do.trace.golden index b2c41ce010..ee2faa6d0b 100644 --- a/internal/exec/runtime/testdata/conformance/state_concurrent_do.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_concurrent_do.trace.golden @@ -3,6 +3,7 @@ enter: lwork transition: lstart -> lwork do: lwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 0 eval literal 10 -> 10 @@ -22,6 +23,7 @@ do: lwork eval operator + -> 12 do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 12 eval literal 10 -> 10 @@ -36,6 +38,7 @@ do: lwork eval operator * -> 1240 eval literal 3 -> 3 eval operator + -> 1243 +leave action node: state behavior do: rwork stmt assign seq eval feature seq -> 1243 @@ -53,6 +56,7 @@ do: rwork eval operator * -> 124350 eval literal 6 -> 6 eval operator + -> 124356 +leave action node: state behavior exit: rwork enter: rdone transition: rwork -> rdone diff --git a/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.declared.trace.golden index 6c026245d4..ebd9304055 100644 --- a/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.declared.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.declared.trace.golden @@ -3,6 +3,7 @@ enter: lwork transition: lstart -> lwork do: lwork stmt action body +enter action node: state behavior stmt for i eval literal 1 -> 1 eval literal 2 -> 2 @@ -28,6 +29,7 @@ do: lwork eval operator + -> 12 do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 12 eval literal 10 -> 10 @@ -42,11 +44,13 @@ do: lwork eval operator * -> 1240 eval literal 3 -> 3 eval operator + -> 1243 +leave action node: state behavior do: rwork stmt if eval feature seq -> 1243 eval literal 0 -> 0 eval operator > -> true +enter action node: branch body of state behavior stmt assign seq eval feature seq -> 1243 eval literal 10 -> 10 @@ -63,6 +67,8 @@ do: rwork eval operator * -> 124350 eval literal 6 -> 6 eval operator + -> 124356 +leave action node: branch body of state behavior +leave action node: state behavior exit: rwork enter: rdone transition: rwork -> rdone diff --git a/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.seed-1.trace.golden index a0617c1fc1..654c968173 100644 --- a/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.seed-1.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.seed-1.trace.golden @@ -3,6 +3,7 @@ enter: rwork transition: rstart -> rwork do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 0 eval literal 10 -> 10 @@ -18,6 +19,7 @@ do: rwork eval feature seq -> 4 eval literal 0 -> 0 eval operator > -> true +enter action node: branch body of state behavior stmt assign seq eval feature seq -> 4 eval literal 10 -> 10 @@ -26,6 +28,7 @@ do: rwork eval operator + -> 45 do: lwork stmt action body +enter action node: state behavior stmt for i eval literal 1 -> 1 eval literal 2 -> 2 @@ -45,6 +48,8 @@ do: rwork eval operator * -> 4510 eval literal 6 -> 6 eval operator + -> 4516 +leave action node: branch body of state behavior +leave action node: state behavior do: lwork iteration 2 stmt assign seq @@ -63,6 +68,7 @@ do: lwork eval operator * -> 451620 eval literal 3 -> 3 eval operator + -> 451623 +leave action node: state behavior exit: lwork enter: ldone transition: lwork -> ldone diff --git a/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.sysml b/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.sysml index 6e7a6f1ee0..1c86a01380 100644 --- a/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.sysml +++ b/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.sysml @@ -18,7 +18,7 @@ package Test { for i in 1..2 { assign seq := seq * 10 + i; } - assign seq := seq * 10 + 3; + then assign seq := seq * 10 + 3; } } state ldone; @@ -33,9 +33,9 @@ package Test { state rwork { do action { assign seq := seq * 10 + 4; - if seq > 0 { + then if seq > 0 { assign seq := seq * 10 + 5; - assign seq := seq * 10 + 6; + then assign seq := seq * 10 + 6; } } } diff --git a/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.trace.golden b/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.trace.golden index 6c026245d4..ebd9304055 100644 --- a/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_concurrent_inline_do_bodies.trace.golden @@ -3,6 +3,7 @@ enter: lwork transition: lstart -> lwork do: lwork stmt action body +enter action node: state behavior stmt for i eval literal 1 -> 1 eval literal 2 -> 2 @@ -28,6 +29,7 @@ do: lwork eval operator + -> 12 do: rwork stmt action body +enter action node: state behavior stmt assign seq eval feature seq -> 12 eval literal 10 -> 10 @@ -42,11 +44,13 @@ do: lwork eval operator * -> 1240 eval literal 3 -> 3 eval operator + -> 1243 +leave action node: state behavior do: rwork stmt if eval feature seq -> 1243 eval literal 0 -> 0 eval operator > -> true +enter action node: branch body of state behavior stmt assign seq eval feature seq -> 1243 eval literal 10 -> 10 @@ -63,6 +67,8 @@ do: rwork eval operator * -> 124350 eval literal 6 -> 6 eval operator + -> 124356 +leave action node: branch body of state behavior +leave action node: state behavior exit: rwork enter: rdone transition: rwork -> rdone diff --git a/internal/exec/runtime/testdata/conformance/state_do_body_interrupted_by_signal.sysml b/internal/exec/runtime/testdata/conformance/state_do_body_interrupted_by_signal.sysml index 8d984a9e7d..1c9a7d62da 100644 --- a/internal/exec/runtime/testdata/conformance/state_do_body_interrupted_by_signal.sysml +++ b/internal/exec/runtime/testdata/conformance/state_do_body_interrupted_by_signal.sysml @@ -14,8 +14,8 @@ package Test { state busy { do action work { assign log := log + "s1 "; - assign log := log + "s2 "; - assign log := log + "s3 "; + then assign log := log + "s2 "; + then assign log := log + "s3 "; } exit action leave { assign log := log + "exit "; diff --git a/internal/exec/runtime/testdata/conformance/state_do_body_interrupted_by_signal.trace.golden b/internal/exec/runtime/testdata/conformance/state_do_body_interrupted_by_signal.trace.golden index df0ba901ca..266156deee 100644 --- a/internal/exec/runtime/testdata/conformance/state_do_body_interrupted_by_signal.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_do_body_interrupted_by_signal.trace.golden @@ -1,5 +1,6 @@ do: busy stmt action body +enter action node: state behavior work stmt assign log eval feature log -> "" eval literal "s1 " -> "s1 " diff --git a/internal/exec/runtime/testdata/conformance/state_do_body_unordered_join_once.sysml b/internal/exec/runtime/testdata/conformance/state_do_body_unordered_join_once.sysml index 06a5a4553d..35c01f7386 100644 --- a/internal/exec/runtime/testdata/conformance/state_do_body_unordered_join_once.sysml +++ b/internal/exec/runtime/testdata/conformance/state_do_body_unordered_join_once.sysml @@ -17,8 +17,8 @@ package test { action b { assign total := total + 10; assign doneB := true; } action c { assign seen := total; - assign cRuns := cRuns + 1; - assign total := total + 100; + then assign cRuns := cRuns + 1; + then assign total := total + 100; } succession first a then c; succession first b then c; diff --git a/internal/exec/runtime/testdata/conformance/state_do_body_unordered_join_once.trace.golden b/internal/exec/runtime/testdata/conformance/state_do_body_unordered_join_once.trace.golden index b87fefbc03..56ea174221 100644 --- a/internal/exec/runtime/testdata/conformance/state_do_body_unordered_join_once.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_do_body_unordered_join_once.trace.golden @@ -17,17 +17,22 @@ enter action node: state behavior ops stmt assign doneA eval literal true -> true choice step 1: tokens 1@a, 2@b (unordered; took 2@b first) +do: active +enter action node: c do: active stmt assign seen eval feature total -> 11 +do: active stmt assign cRuns eval feature cRuns -> 0 eval literal 1 -> 1 eval operator + -> 1 +do: active stmt assign total eval feature total -> 11 eval literal 100 -> 100 eval operator + -> 111 +leave action node: c leave action node: state behavior ops exit: active enter: done diff --git a/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.check.expected.json b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.check.expected.json new file mode 100644 index 0000000000..5bf7cca6f9 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"c": ["1", "2"]}, + "agreed": {"finalState": "finished"} +} diff --git a/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.declared.trace.golden new file mode 100644 index 0000000000..357196928d --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.declared.trace.golden @@ -0,0 +1,28 @@ +exit: idle +enter: work +transition: idle -> work +do: work +stmt action body +enter action node: state behavior + stmt assign c + eval literal 0 -> 0 +do: work + stmt if + eval literal true -> true +enter action node: branch body of state behavior + eval feature c -> 0 + stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 + eval feature c -> 1 + stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +choice step 2: tokens 3@a, 4@b (unordered; took 3@a first) +leave action node: branch body of state behavior +leave action node: state behavior +exit: work +enter: finished +transition: work -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.expected.json b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.expected.json new file mode 100644 index 0000000000..9ba72e13ca --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.expected.json @@ -0,0 +1,10 @@ +{ + "type": "state", + "libraries": true, + "trace": true, + "outcomes": [ + {"finalState": "finished", "outputs": {"c": {"type": "Integer", "value": 1}}}, + {"finalState": "finished", "outputs": {"c": {"type": "Integer", "value": 2}}} + ], + "admissible": "A leaf body's start shot and its assignments: another performance may run between them" +} diff --git a/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.seed-1.trace.golden new file mode 100644 index 0000000000..357196928d --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.seed-1.trace.golden @@ -0,0 +1,28 @@ +exit: idle +enter: work +transition: idle -> work +do: work +stmt action body +enter action node: state behavior + stmt assign c + eval literal 0 -> 0 +do: work + stmt if + eval literal true -> true +enter action node: branch body of state behavior + eval feature c -> 0 + stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 + eval feature c -> 1 + stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +choice step 2: tokens 3@a, 4@b (unordered; took 3@a first) +leave action node: branch body of state behavior +leave action node: state behavior +exit: work +enter: finished +transition: work -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.sysml b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.sysml new file mode 100644 index 0000000000..66abdea5c3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.sysml @@ -0,0 +1,39 @@ +package test { + private import ScalarValues::*; + + state def DoFlowBranchFork { + attribute c : Integer := 0; + // Oracle (docs/project/behavior-semantic-oracle.md): the `if` node of the do body's + // stated flow forks two leaf bodies; either may start between the other's snapshot + // and its write, as in a statement-list body. + entry; then idle; + state idle; + state work { + do action { + assign c := 0; + then if true { + first start; + then fork f; + then a; + then b; + action a { + attribute t : Integer := c; + assign c := t + 1; + } + action b { + attribute t : Integer := c; + assign c := t + 1; + } + succession a then j; + succession b then j; + join j; + then done; + } + } + } + state finished; + + succession first idle then work; + succession first work then finished; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.trace.golden b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.trace.golden new file mode 100644 index 0000000000..b64ea6962a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_do_flow_branch_fork_lost_update.trace.golden @@ -0,0 +1,28 @@ +exit: idle +enter: work +transition: idle -> work +do: work +stmt action body +enter action node: state behavior + stmt assign c + eval literal 0 -> 0 +do: work + stmt if + eval literal true -> true +enter action node: branch body of state behavior + eval feature c -> 0 + stmt assign c + eval feature t -> 0 + eval literal 1 -> 1 + eval operator + -> 1 + eval feature c -> 1 + stmt assign c + eval feature t -> 1 + eval literal 1 -> 1 + eval operator + -> 2 +choice step 2: tokens 3@a, 4@b (unordered; took 4@b first) +leave action node: branch body of state behavior +leave action node: state behavior +exit: work +enter: finished +transition: work -> finished diff --git a/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.declared.trace.golden index 1a13ddbe7e..f7b618fcfb 100644 --- a/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.declared.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.declared.trace.golden @@ -11,6 +11,7 @@ stmt action body transition: idle -> work (event: accept Go) do: l1 stmt action body +enter action node: state behavior stmt assign log eval feature log -> "r1(entry) " eval literal "a " -> "a " diff --git a/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.seed-1.trace.golden index 89892e5e69..b089e7ccd0 100644 --- a/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.seed-1.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.seed-1.trace.golden @@ -11,6 +11,7 @@ enter: l1 transition: idle -> work (event: accept Go) do: l1 stmt action body +enter action node: state behavior stmt assign log eval feature log -> "r1(entry) " eval literal "a " -> "a " diff --git a/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.sysml b/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.sysml index 87bfa56ae6..9f5e27a672 100644 --- a/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.sysml +++ b/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.sysml @@ -12,7 +12,7 @@ package Test { state left { entry; then l1; state l1 { - do { assign log := log + "a "; assign log := log + "b "; } + do { assign log := log + "a "; then assign log := log + "b "; } } } state right { diff --git a/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.trace.golden b/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.trace.golden index 1a13ddbe7e..f7b618fcfb 100644 --- a/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_do_step_cut_by_sibling_completion.trace.golden @@ -11,6 +11,7 @@ stmt action body transition: idle -> work (event: accept Go) do: l1 stmt action body +enter action node: state behavior stmt assign log eval feature log -> "r1(entry) " eval literal "a " -> "a " diff --git a/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.declared.trace.golden b/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.declared.trace.golden index 849d43a443..1727330f1a 100644 --- a/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.declared.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.declared.trace.golden @@ -7,12 +7,14 @@ stmt assign log choice events at t=1.0: time c1 1->c2, time d1 1->d2 (unordered; dispatched time c1 1->c2 first) exit: c1 stmt action body +enter action node: state behavior stmt assign log eval feature log -> "b " eval literal "c " -> "c " eval operator + -> "b c " stmt send materialize: Go #1 +leave action node: state behavior enter: c2 transition: c1 -> c2 (event: time) choice events at t=1.0: time d1 1->d2, accept Go (unordered; dispatched time d1 1->d2 first) diff --git a/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.seed-1.trace.golden index 849d43a443..1727330f1a 100644 --- a/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.seed-1.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.seed-1.trace.golden @@ -7,12 +7,14 @@ stmt assign log choice events at t=1.0: time c1 1->c2, time d1 1->d2 (unordered; dispatched time c1 1->c2 first) exit: c1 stmt action body +enter action node: state behavior stmt assign log eval feature log -> "b " eval literal "c " -> "c " eval operator + -> "b c " stmt send materialize: Go #1 +leave action node: state behavior enter: c2 transition: c1 -> c2 (event: time) choice events at t=1.0: time d1 1->d2, accept Go (unordered; dispatched time d1 1->d2 first) diff --git a/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.sysml b/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.sysml index df1c9edd80..351c67a9d7 100644 --- a/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.sysml +++ b/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.sysml @@ -25,7 +25,7 @@ package Test { entry; then c1; state c1; state c2; - transition first c1 accept after 1 [s] do action { assign log := log + "c "; send new Go() to Machine; } then c2; + transition first c1 accept after 1 [s] do action { assign log := log + "c "; then send new Go() to Machine; } then c2; } state d { entry; then d1; diff --git a/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.trace.golden b/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.trace.golden index 849d43a443..1727330f1a 100644 --- a/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_join_time_segment_not_taken_by_signal.trace.golden @@ -7,12 +7,14 @@ stmt assign log choice events at t=1.0: time c1 1->c2, time d1 1->d2 (unordered; dispatched time c1 1->c2 first) exit: c1 stmt action body +enter action node: state behavior stmt assign log eval feature log -> "b " eval literal "c " -> "c " eval operator + -> "b c " stmt send materialize: Go #1 +leave action node: state behavior enter: c2 transition: c1 -> c2 (event: time) choice events at t=1.0: time d1 1->d2, accept Go (unordered; dispatched time d1 1->d2 first) diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_after_accept.sysml b/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_after_accept.sysml index 939a547ff4..afd8f71b5e 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_after_accept.sysml +++ b/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_after_accept.sysml @@ -18,10 +18,10 @@ package Test { state s { do { action wait { first start; then action w accept Go; then done; } - assign d := 1; - terminate; - action again { first start; then action w accept Go; then done; } - assign d := 9; + then assign d := 1; + then terminate; + then action again { first start; then action w accept Go; then done; } + then assign d := 9; } } state t; diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_after_accept.trace.golden b/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_after_accept.trace.golden index 2f880e2b27..aac37b497d 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_after_accept.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_after_accept.trace.golden @@ -1,15 +1,15 @@ do: s stmt action body - stmt node wait +enter action node: state behavior enter action node: wait do: s leave action node: wait -do: s stmt assign d eval literal 1 -> 1 do: s stmt terminate -terminate state behavior : no token dropped +terminate state behavior : dropped token 1@terminate +leave action node: state behavior exit: s enter: t eval literal 1 -> 1 diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_in_one_region.sysml b/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_in_one_region.sysml index d1716bad86..8a5094e69a 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_in_one_region.sysml +++ b/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_in_one_region.sysml @@ -13,8 +13,8 @@ package Test { state lwork { do { assign a := 1; - terminate; - assign a := 9; + then terminate; + then assign a := 9; } } state ldone; @@ -26,8 +26,8 @@ package Test { state rwork { do { assign b := 1; - assign b := b + 1; - assign b := b + 1; + then assign b := b + 1; + then assign b := b + 1; } } state rdone; diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_in_one_region.trace.golden b/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_in_one_region.trace.golden index bf56f54b52..7b27605b2c 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_in_one_region.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_terminate_braced_do_in_one_region.trace.golden @@ -1,16 +1,19 @@ choice do round at t=0.0: states lwork, rwork react (unordered; took lwork first) do: lwork stmt action body +enter action node: state behavior stmt assign a eval literal 1 -> 1 do: rwork stmt action body +enter action node: state behavior stmt assign b eval literal 1 -> 1 choice do round at t=0.0: states lwork, rwork react (unordered; took lwork first) do: lwork stmt terminate -terminate state behavior : no token dropped +terminate state behavior : dropped token 1@terminate +leave action node: state behavior do: rwork stmt assign b eval feature b -> 1 @@ -24,6 +27,7 @@ do: rwork eval feature b -> 2 eval literal 1 -> 1 eval operator + -> 3 +leave action node: state behavior exit: rwork enter: rdone transition: rwork -> rdone diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_among_named.sysml b/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_among_named.sysml index 3e9279204a..c22da9b8ff 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_among_named.sysml +++ b/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_among_named.sysml @@ -12,7 +12,7 @@ package Test { state s { entry action head { assign log := log + "head;"; } - entry { assign a := 1; terminate; assign a := 9; } + entry { assign a := 1; then terminate; then assign a := 9; } entry action tail { assign log := log + "tail;"; } } } diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_do_exit_effect.sysml b/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_do_exit_effect.sysml index 71edfc0363..c136cfabbf 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_do_exit_effect.sysml +++ b/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_do_exit_effect.sysml @@ -16,14 +16,14 @@ package test { entry; then s; state s { - entry { assign e := 1; terminate; assign e := 9; } - do { assign d := 1; terminate; assign d := 9; } - exit { assign x := 1; terminate; assign x := 9; } + entry { assign e := 1; then terminate; then assign e := 9; } + do { assign d := 1; then terminate; then assign d := 9; } + exit { assign x := 1; then terminate; then assign x := 9; } } state t; transition first s accept after 1 [SI::s] - do { assign n := 1; terminate; assign n := 9; } + do { assign n := 1; then terminate; then assign n := 9; } then t; } } diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_do_exit_effect.trace.golden b/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_do_exit_effect.trace.golden index 84db0bbc28..1fcd09d76d 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_do_exit_effect.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_terminate_braced_entry_do_exit_effect.trace.golden @@ -1,20 +1,26 @@ do: s stmt action body +enter action node: state behavior stmt assign d eval literal 1 -> 1 do: s stmt terminate -terminate state behavior : no token dropped +terminate state behavior : dropped token 1@terminate +leave action node: state behavior exit: s (exit action) stmt action body +enter action node: state behavior stmt assign x eval literal 1 -> 1 stmt terminate -terminate state behavior : no token dropped +terminate state behavior : dropped token 1@terminate +leave action node: state behavior stmt action body +enter action node: state behavior stmt assign n eval literal 1 -> 1 stmt terminate -terminate state behavior : no token dropped +terminate state behavior : dropped token 1@terminate +leave action node: state behavior enter: t transition: s -> t (event: time) diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_braced_inherited_by_two_usages.sysml b/internal/exec/runtime/testdata/conformance/state_terminate_braced_inherited_by_two_usages.sysml index fb7e7e5b9b..f95f34b3b3 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_braced_inherited_by_two_usages.sysml +++ b/internal/exec/runtime/testdata/conformance/state_terminate_braced_inherited_by_two_usages.sysml @@ -13,13 +13,13 @@ package Test { entry; then idle; state idle { - entry { assign log := log + "e1;"; terminate; assign log := log + "e9;"; } - do { assign log := log + "d1;"; terminate; assign log := log + "d9;"; } - exit { assign log := log + "x1;"; terminate; assign log := log + "x9;"; } + entry { assign log := log + "e1;"; then terminate; then assign log := log + "e9;"; } + do { assign log := log + "d1;"; then terminate; then assign log := log + "d9;"; } + exit { assign log := log + "x1;"; then terminate; then assign log := log + "x9;"; } } state done; transition first idle accept Next - do { assign log := log + "n1;"; terminate; assign log := log + "n9;"; } + do { assign log := log + "n1;"; then terminate; then assign log := log + "n9;"; } then done; } diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_entry_do_exit_behaviors.sysml b/internal/exec/runtime/testdata/conformance/state_terminate_entry_do_exit_behaviors.sysml index 1ed15b207a..51fe5d9195 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_entry_do_exit_behaviors.sysml +++ b/internal/exec/runtime/testdata/conformance/state_terminate_entry_do_exit_behaviors.sysml @@ -13,18 +13,18 @@ package test { state idle { entry action begin { assign count := 1; - terminate; - assign count := 99; + then terminate; + then assign count := 99; } do action tick { assign later := 5; - terminate; - assign later := 77; + then terminate; + then assign later := 77; } exit action leave { assign count := count + 10; - terminate; - assign count := 0; + then terminate; + then assign count := 0; } } state next; diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_entry_do_exit_behaviors.trace.golden b/internal/exec/runtime/testdata/conformance/state_terminate_entry_do_exit_behaviors.trace.golden index 6a77c9f46d..24853c4da1 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_entry_do_exit_behaviors.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_terminate_entry_do_exit_behaviors.trace.golden @@ -1,17 +1,21 @@ do: idle stmt action body +enter action node: state behavior tick stmt assign later eval literal 5 -> 5 do: idle stmt terminate -terminate state behavior tick: no token dropped +terminate state behavior tick: dropped token 1@terminate +leave action node: state behavior tick exit: idle (exit action) stmt action body +enter action node: state behavior leave stmt assign count eval feature count -> 1 eval literal 10 -> 10 eval operator + -> 11 stmt terminate -terminate state behavior leave: no token dropped +terminate state behavior leave: dropped token 1@terminate +leave action node: state behavior leave enter: next transition: idle -> next (event: time) diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_this_ends_performer.sysml b/internal/exec/runtime/testdata/conformance/state_terminate_this_ends_performer.sysml index ebdfba7caf..a5056d52c4 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_this_ends_performer.sysml +++ b/internal/exec/runtime/testdata/conformance/state_terminate_this_ends_performer.sysml @@ -17,8 +17,8 @@ package test { state busy { do action work { assign count := 1; - terminate this; - assign count := 2; + then terminate this; + then assign count := 2; } exit action leave { assign exits := 1; diff --git a/internal/exec/runtime/testdata/conformance/state_terminate_this_ends_performer.trace.golden b/internal/exec/runtime/testdata/conformance/state_terminate_this_ends_performer.trace.golden index 14c36d0169..591eecff4f 100644 --- a/internal/exec/runtime/testdata/conformance/state_terminate_this_ends_performer.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_terminate_this_ends_performer.trace.golden @@ -5,10 +5,12 @@ enter: busy run: exhibited state machine cycle of #1 do: busy stmt action body +enter action node: state behavior work stmt assign count eval literal 1 -> 1 do: busy stmt terminate eval feature this -> instance#1 terminate: Worker #1 +terminate state behavior work: dropped token 1@terminate this terminated with occurrence: cycle (do behavior abandoned: busy) diff --git a/internal/exec/runtime/testdata/conformance/state_transition_braced_do_then_accept.sysml b/internal/exec/runtime/testdata/conformance/state_transition_braced_do_then_accept.sysml index 771c013da9..f6fb807fc1 100644 --- a/internal/exec/runtime/testdata/conformance/state_transition_braced_do_then_accept.sysml +++ b/internal/exec/runtime/testdata/conformance/state_transition_braced_do_then_accept.sysml @@ -17,12 +17,12 @@ package Test { transition first idle accept Go do { assign log := log * 10 + 1; - assign log := log * 10 + 2; - assign log := log * 10 + 3; + then assign log := log * 10 + 2; + then assign log := log * 10 + 3; } then busy; transition first busy accept Go do { assign log := log * 10 + 4; - assign log := log * 10 + 5; + then assign log := log * 10 + 5; } then done; } } diff --git a/internal/exec/runtime/testdata/conformance/state_transition_braced_do_then_accept.trace.golden b/internal/exec/runtime/testdata/conformance/state_transition_braced_do_then_accept.trace.golden index 6fd7da773b..b8eb952851 100644 --- a/internal/exec/runtime/testdata/conformance/state_transition_braced_do_then_accept.trace.golden +++ b/internal/exec/runtime/testdata/conformance/state_transition_braced_do_then_accept.trace.golden @@ -1,5 +1,6 @@ exit: idle stmt action body +enter action node: state behavior stmt assign log eval feature log -> 0 eval literal 10 -> 10 @@ -18,10 +19,12 @@ stmt action body eval operator * -> 120 eval literal 3 -> 3 eval operator + -> 123 +leave action node: state behavior enter: busy transition: idle -> busy (event: accept Go) exit: busy stmt action body +enter action node: state behavior stmt assign log eval feature log -> 123 eval literal 10 -> 10 @@ -34,5 +37,6 @@ stmt action body eval operator * -> 12340 eval literal 5 -> 5 eval operator + -> 12345 +leave action node: state behavior enter: done transition: busy -> done (event: accept Go) diff --git a/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.check.expected.json b/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.check.expected.json new file mode 100644 index 0000000000..3d083a69fc --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.check.expected.json @@ -0,0 +1,4 @@ +{ + "verdict": "divergent", + "divergent": {"finalState": ["accepted", "start"]} +} diff --git a/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.expected.json b/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.expected.json new file mode 100644 index 0000000000..b42336765c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.expected.json @@ -0,0 +1,10 @@ +{ + "type": "state", + "evaluate": "test::TransitionGuard", + "events": [{"signal": "Go"}], + "outcomes": [ + {"finalState": "accepted", "stateVisits": ["start", "accepted"], "outputs": {}}, + {"finalState": "start", "stateVisits": ["start"], "outputs": {}} + ], + "admissible": "Direct statements of one body no succession orders: each is performed, in which order is open" +} diff --git a/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.sysml b/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.sysml new file mode 100644 index 0000000000..89f1bd0967 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_transition_guard_statement_order.sysml @@ -0,0 +1,20 @@ +package test { + private import ScalarValues::*; + item def Go; + + constraint def Ok { + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y == 12 + } + + state def TransitionGuard { + entry; then start; + state start; + state accepted; + state rejected; + + transition first start accept Go if Ok() then accepted; + } +} diff --git a/internal/exec/runtime/testdata/conformance/verification_explore_step_order.expected.json b/internal/exec/runtime/testdata/conformance/verification_explore_step_order.expected.json new file mode 100644 index 0000000000..353f958e85 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/verification_explore_step_order.expected.json @@ -0,0 +1,10 @@ +{ + "libraries": true, + "type": "verification", + "evaluate": "test::OrderCheck", + "schedule": "reverse", + "verdict": "pass", + "verdicts": { + "order": "satisfied" + } +} diff --git a/internal/exec/runtime/testdata/conformance/verification_explore_step_order.sysml b/internal/exec/runtime/testdata/conformance/verification_explore_step_order.sysml new file mode 100644 index 0000000000..b87eedc718 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/verification_explore_step_order.sysml @@ -0,0 +1,21 @@ +package test { + private import ScalarValues::*; + private import VerificationCases::*; + + verification def OrderCheck { + return verdict : VerdictKind; + attribute x : Integer := 1; + objective order { + require constraint { x == 12 } + } + action s1 { assign x := x * 10; } + action s2 { assign x := x + 2; } + VerificationCases::PassIf(x == 12) + } + + action def Harness { + out attribute verdict : VerdictKind := VerdictKind::pass; + verification run : OrderCheck; + then assign verdict := run.verdict; + } +} diff --git a/internal/exec/runtime/testdata/conformance/verification_explore_step_order_declared.expected.json b/internal/exec/runtime/testdata/conformance/verification_explore_step_order_declared.expected.json new file mode 100644 index 0000000000..c5476b4dc3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/verification_explore_step_order_declared.expected.json @@ -0,0 +1,10 @@ +{ + "libraries": true, + "type": "verification", + "evaluate": "test::OrderCheck", + "schedule": "declared", + "verdict": "pass", + "verdicts": { + "order": "satisfied" + } +} diff --git a/internal/exec/runtime/testdata/conformance/verification_explore_step_order_declared.sysml b/internal/exec/runtime/testdata/conformance/verification_explore_step_order_declared.sysml new file mode 100644 index 0000000000..c93d049bc7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/verification_explore_step_order_declared.sysml @@ -0,0 +1,15 @@ +package test { + private import ScalarValues::*; + private import VerificationCases::*; + + verification def OrderCheck { + return verdict : VerdictKind; + attribute x : Integer := 1; + objective order { + require constraint { x == 12 } + } + action s1 { assign x := x * 10; } + action s2 { assign x := x + 2; } + VerificationCases::PassIf(x == 12) + } +} diff --git a/internal/exec/runtime/testdata/conformance/w7d_send_via_port_to_receiver.seed-1.trace.golden b/internal/exec/runtime/testdata/conformance/w7d_send_via_port_to_receiver.seed-1.trace.golden index e31670e249..c2f65f4cfd 100644 --- a/internal/exec/runtime/testdata/conformance/w7d_send_via_port_to_receiver.seed-1.trace.golden +++ b/internal/exec/runtime/testdata/conformance/w7d_send_via_port_to_receiver.seed-1.trace.golden @@ -1,16 +1,17 @@ step 1: token 1@sender -stmt send - eval literal 42 -> 42 +choice statements in action node sender: next 1 send, 2 send (unordered; took 2 send first) stmt send eval literal 7 -> 7 +stmt send + eval literal 42 -> 42 step 2: token 1@split step 3: token 2@receiver, token 3@sibling -stmt assign receiverGot - eval feature value -> 42 stmt assign siblingGot eval feature value -> 7 -choice step 4: writes value := 42 by token 2, value := 7 by token 3 (unordered; value := 7 by token 3 stood) -choice step 4: tokens 2@receiver, 3@sibling (unordered; took 2@receiver first) +stmt assign receiverGot + eval feature value -> 42 +choice step 4: writes value := 42 by token 2, value := 7 by token 3 (unordered; value := 42 by token 2 stood) +choice step 4: tokens 2@receiver, 3@sibling (unordered; took 3@sibling first) step 4: token 2@sync, token 3@sync step 5: token 4@done step 6: no active tokens diff --git a/internal/exec/runtime/testdata/robustness/action_explore_body_callee_outputs.sysml b/internal/exec/runtime/testdata/robustness/action_explore_body_callee_outputs.sysml new file mode 100644 index 0000000000..e4618d3d67 --- /dev/null +++ b/internal/exec/runtime/testdata/robustness/action_explore_body_callee_outputs.sysml @@ -0,0 +1,29 @@ +package test { + private import ScalarValues::*; + + // Twice writes its output twice; the caller's pin takes each write as it is made. + action def Twice { + out value : Integer := 0; + first start then w1; + action w1 { assign value := 1; } + then w2; + action w2 { assign value := 2; } + then done; + } + + // reader, forked beside producer, may read its pin before, between or after + // Twice's two writes, or before producer is performed at all, a typed error. + action CalleeOutputs { + attribute seen : Integer := 0; + first start; + then fork f; + then producer; + then reader; + action producer : Twice; + action reader { assign seen := producer.value; } + succession producer then j; + succession reader then j; + join j; + then done; + } +} diff --git a/internal/exec/runtime/testdata/robustness/action_explore_body_sibling_timer.sysml b/internal/exec/runtime/testdata/robustness/action_explore_body_sibling_timer.sysml new file mode 100644 index 0000000000..b22671de01 --- /dev/null +++ b/internal/exec/runtime/testdata/robustness/action_explore_body_sibling_timer.sysml @@ -0,0 +1,39 @@ +package test { + private import ScalarValues::*; + private import SI::*; + action def Reader { + out attribute timed : Integer = 0; + first start; + then action timer accept after 10 [s]; + then action noteTimer { assign timed := 1; } + then done; + } + action def Main { + out attribute timed : Integer = 0; + first start; + then perform action reader : Reader; + then action collect { assign timed := reader.timed; } + then done; + } + // gate's timer starts at the same instant as reader's, so mark reads main.timed at 1 s. + action def SiblingTimer { + out attribute timed : Integer = 0; + out attribute gated : Integer = 0; + first start; + fork split; + perform action main : Main; + action gate accept after 1 [s]; + action mark { assign gated := main.timed + 1; } + join meet; + action collect { assign timed := main.timed; } + done; + succession first start then split; + succession first split then main; + succession first split then gate; + succession first gate then mark; + succession first mark then meet; + succession first main then meet; + succession first meet then collect; + succession first collect then done; + } +} diff --git a/internal/exec/smt/atomic_body_order_test.go b/internal/exec/smt/atomic_body_order_test.go new file mode 100644 index 0000000000..fcd396339e --- /dev/null +++ b/internal/exec/smt/atomic_body_order_test.go @@ -0,0 +1,92 @@ +package smt + +import ( + "strings" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/exec/analysis" + "github.com/Open-MBEE/OpenSysML/internal/exec/solve" +) + +func TestEngineDoesNotCoverAtomicBodyStatementOrder(t *testing.T) { + tests := []struct { + name, source, behavior, condition, reason string + }{ + { + name: "action invokes a reordering calc", + reason: ErrNotEncoded.Error(), + source: `package test { + private import ScalarValues::*; + calc def Ord { + return : Integer; + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y + } + action def Use { + out attribute r : Integer := 0; + assign r := Ord(); + } + }`, + behavior: "test::Use", + }, + { + name: "calc usage has a reordering body", + reason: ErrNotEncoded.Error(), + source: `package test { + private import ScalarValues::*; + calc def Ord { + return : Integer; + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y + } + calc def Caller { + return : Integer; + calc ord : Ord; + ord + } + action def Use { + out attribute r : Integer := 0; + assign r := Caller(); + } + }`, + behavior: "test::Use", + }, + { + name: "constraint body has steps", + reason: "constraint body steps not translatable for solving", + source: `package test { + private import ScalarValues::*; + action def A { + first start; + done; + succession first start then done; + constraint ok { + attribute y : Integer := 1; + assign y := y * 10; + assign y := y + 2; + y == 12 + } + } + }`, + behavior: "test::A", + condition: "test::A::ok", + }, + } + e := New(func() (*solve.Solver, error) { + return &solve.Solver{Name: "unavailable", Path: "/nonexistent"}, nil + }) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + d := indexed(t, "atomic_body_order.sysml", test.source) + result := answer(t, e, d, d.holds(t, test.behavior, test.condition), analysis.Budget{Depth: 4}) + expect(t, result, analysis.ClaimNone, analysis.NotCovered) + if !strings.Contains(result.Reason, test.reason) { + t.Fatalf("reason %q does not identify the unsupported encoding", result.Reason) + } + }) + } +} diff --git a/internal/exec/smt/support.go b/internal/exec/smt/support.go index 1231cbd228..271551fc5d 100644 --- a/internal/exec/smt/support.go +++ b/internal/exec/smt/support.go @@ -180,6 +180,9 @@ func Analyze(graph *lower.ActionGraph, model *semantics.Model, k int) (*Flow, er if err := f.checkImplicitJoin(node); err != nil { return nil, err } + if err := f.checkBodyInterleaving(node); err != nil { + return nil, err + } } } if f.Slots > MaxSlots { @@ -303,7 +306,7 @@ func (f *Flow) checkNode(node ast.Node) error { f.Delivers = true } } - return f.checkBody(node, label, graph.Bodies[node]) + return f.checkBody(graph, node, label, graph.Bodies[node]) } // refuseNested refuses a node stating a flow of its own, whatever that flow holds. @@ -402,9 +405,23 @@ func (f *Flow) checkImplicitJoin(node ast.Node) error { Reason: "a node several successions enter synchronizes over those still reachable while tokens run concurrently; only a join or a merge is encoded there"} } +// checkBodyInterleaving refuses a body another token's moves may interleave +// inside while tokens run concurrently: the encoding performs a body as one move. +func (f *Flow) checkBodyInterleaving(node ast.Node) error { + if !lower.BodyDivides(f.FrameOf[node].Graph, node) { + return nil + } + return &UnsupportedError{Node: f.label(node), Construct: "body interleaving", + Reason: "another performance may interleave between this body's start and its statements, or between two of them; the encoding performs a body as one move"} +} + // checkBody refuses the statements of a body the stage does not encode, and // records the loops it unrolls. -func (f *Flow) checkBody(node ast.Node, label string, body []lower.Statement) error { +func (f *Flow) checkBody(graph *lower.ActionGraph, node ast.Node, label string, body []lower.Statement) error { + if lower.BodyStatementOrder(graph, node, body).Reorders(false) { + return &UnsupportedError{Node: label, Construct: "statement order", + Reason: "two statements no succession orders depend on each other; the encoding performs them in declaration order"} + } for _, stmt := range body { switch s := stmt.(type) { case lower.Assign: @@ -418,15 +435,15 @@ func (f *Flow) checkBody(node ast.Node, label string, body []lower.Statement) er case lower.DeclareUsage: return &UnsupportedError{Node: label, Construct: "usage declaration", Reason: "a body declaring a usage is not encoded"} case lower.Block: - if err := f.checkBlock(node, label, s); err != nil { + if err := f.checkBlock(graph, node, label, s); err != nil { return err } case lower.If: - if err := f.checkBlock(node, label, s.Then); err != nil { + if err := f.checkBlock(graph, node, label, s.Then); err != nil { return err } if s.Else != nil { - if err := f.checkBlock(node, label, *s.Else); err != nil { + if err := f.checkBlock(graph, node, label, *s.Else); err != nil { return err } } @@ -437,7 +454,7 @@ func (f *Flow) checkBody(node ast.Node, label string, body []lower.Statement) er if s.Condition == nil && s.Until == nil { return &UnsupportedError{Node: label, Construct: "loop", Reason: "a loop with no condition ends only at the step budget"} } - if err := f.checkBlock(node, label, s.Body); err != nil { + if err := f.checkBlock(graph, node, label, s.Body); err != nil { return err } f.Loops = append(f.Loops, BodyLoop{Node: node, Label: label, Loop: s}) @@ -458,11 +475,11 @@ func (f *Flow) checkBody(node ast.Node, label string, body []lower.Statement) er } // checkBlock refuses a block that runs a flow of its own and checks its statements. -func (f *Flow) checkBlock(node ast.Node, label string, block lower.Block) error { +func (f *Flow) checkBlock(graph *lower.ActionGraph, node ast.Node, label string, block lower.Block) error { if block.Graph != nil { return &UnsupportedError{Node: label, Construct: "nested flow", Reason: "a block declaring action nodes is encoded by a later stage"} } - return f.checkBody(node, label, block.Statements) + return f.checkBody(graph, node, label, block.Statements) } // sizeSlots decides how many tokens the frame's flow may hold at once within k diff --git a/internal/exec/smt/support_test.go b/internal/exec/smt/support_test.go index 6dfa0a1a5a..f94fc933a8 100644 --- a/internal/exec/smt/support_test.go +++ b/internal/exec/smt/support_test.go @@ -492,3 +492,34 @@ func TestAnalyzeRefusesANestedFlowBeforeLookingInside(t *testing.T) { }) } } + +// TestAnalyzeRefusesBodyInterleaving: another fork branch may run between a body's +// start shot and its assignment, which the encoding cannot express, so it refuses the +// flow as not encoded; a body of one assignment runs as one move and is encoded. +func TestAnalyzeRefusesStatementOrder(t *testing.T) { + _, err := Analyze(conformanceAction(t, "action_explore_statement_order_dependent.sysml", "test::Order"), nil, 10) + var unsupported *UnsupportedError + if !errors.As(err, &unsupported) || !errors.Is(err, ErrNotEncoded) { + t.Fatalf("Analyze: got %v, want an UnsupportedError", err) + } + if unsupported.Construct != "statement order" || unsupported.Node != "s" { + t.Errorf("refusal names %q/%q, want node s, construct statement order", unsupported.Node, unsupported.Construct) + } + if _, err := Analyze(conformanceAction(t, "action_explore_statement_order_independent.sysml", "test::Independent"), nil, 10); errors.As(err, &unsupported) && unsupported.Construct == "statement order" { + t.Errorf("Analyze refused a body whose statements commute: %v", err) + } +} + +func TestAnalyzeRefusesBodyInterleaving(t *testing.T) { + _, err := Analyze(conformanceAction(t, "action_explore_body_fork_lost_update.sysml", "test::ForkPlain"), nil, 10) + var unsupported *UnsupportedError + if !errors.As(err, &unsupported) || !errors.Is(err, ErrNotEncoded) { + t.Fatalf("Analyze: got %v, want an UnsupportedError", err) + } + if unsupported.Construct != "body interleaving" || (unsupported.Node != "a" && unsupported.Node != "b") { + t.Errorf("refusal names %q/%q, want node a or b, construct body interleaving", unsupported.Node, unsupported.Construct) + } + if _, err := Analyze(conformanceAction(t, "action_join_waits_for_slowest_branch.sysml", "test::gather"), nil, 10); errors.As(err, &unsupported) && unsupported.Construct == "body interleaving" { + t.Errorf("Analyze refused a flow whose bodies are one move each: %v", err) + } +} diff --git a/internal/exec/solve/objective_test.go b/internal/exec/solve/objective_test.go index c90029268a..d1ce03eb81 100644 --- a/internal/exec/solve/objective_test.go +++ b/internal/exec/solve/objective_test.go @@ -640,7 +640,7 @@ func TestCaseStepsAreNotBodyStatements(t *testing.T) { if !errors.As(err, &refused) { t.Fatalf("translate: %v, want a refusal", err) } - if refused.Construct != "body statement" || refused.Condition != "`action` statement" { - t.Errorf("refused %q (%q), want the body statement's action", refused.Construct, refused.Condition) + if refused.Construct != "constraint body steps" || refused.Condition != "the body's steps then { used <= 3 }" { + t.Errorf("refused %q (%q), want the constraint body steps", refused.Construct, refused.Condition) } } diff --git a/internal/exec/solve/translate.go b/internal/exec/solve/translate.go index b02a6ca7c0..2acb6d37d9 100644 --- a/internal/exec/solve/translate.go +++ b/internal/exec/solve/translate.go @@ -400,8 +400,8 @@ func (t *translator) pinnedAssertions(offset int) []Assertion { // A group stands for the conjunction of its conditions, so negating a group // negates that conjunction. func (t *translator) condition(cond runtime.Condition) (*Term, error) { - if cond.Statement != nil { - return nil, t.refuse(cond.Statement, "body statement", "OpenSysML does not execute a statement in a constraint body") + if cond.Steps != nil { + return nil, t.refuse(cond.Steps.Node, "constraint body steps", "not covered: the solver does not encode the steps a constraint body performs before its result expression") } if cond.Conflict != nil { return nil, t.refuse(cond.Conflict.Node, "conflicting result expression", "only one owned or inherited result expression is allowed") @@ -1091,8 +1091,8 @@ func conditionOrigin(cond runtime.Condition) (*symbols.Symbol, source.Span) { if cond.Expr != nil { span = cond.Expr.Span() } - if cond.Statement != nil { - span = cond.Statement.Span() + if cond.Steps != nil { + span = cond.Steps.Node.Span() } if cond.Conflict != nil { span = cond.Conflict.Node.Span() diff --git a/internal/exec/solve/translate_test.go b/internal/exec/solve/translate_test.go index a1b2d03098..9eaeb831b4 100644 --- a/internal/exec/solve/translate_test.go +++ b/internal/exec/solve/translate_test.go @@ -603,27 +603,28 @@ func TestVariationSortIsShared(t *testing.T) { } } -// TestBodyStatementRefuses: a statement in a constraint body is not executed, so -// the translation refuses rather than solving the conditions as if it ran. +// TestBodyStatementRefuses: the solver does not encode the steps a constraint +// body performs before its result expression, so the translation refuses +// rather than solving the conditions as if the steps ran. func TestBodyStatementRefuses(t *testing.T) { refused := refusal(t, constraintSource(` attribute y : Integer = 1; assign y := 10; y > 5 `), "test::C") - if refused.Construct != "body statement" { - t.Errorf("refused construct is %q, want the body statement", refused.Construct) + if refused.Construct != "constraint body steps" { + t.Errorf("refused construct is %q, want the constraint body steps", refused.Construct) } - if !strings.Contains(refused.Reason, "does not execute") { - t.Errorf("refusal reason is %q, want it to say the statement is not executed", refused.Reason) + if !strings.Contains(refused.Reason, "does not encode the steps") { + t.Errorf("refusal reason is %q, want it to say the steps are not encoded", refused.Reason) } - if refused.Condition != "`assign` statement" { - t.Errorf("refused condition is %q, want the assign statement", refused.Condition) + if refused.Condition != "the body's steps then { y > 5 }" { + t.Errorf("refused condition is %q, want the body's steps and condition", refused.Condition) } } -// TestPerformedActionRefuses: a performed action is a usage rather than a -// statement node, and translation refuses it the same way, nested or not. +// TestPerformedActionRefuses: a performed action is a step of the constraint +// body, and translation refuses the steps the same way, nested or not. func TestPerformedActionRefuses(t *testing.T) { src := ` package test { @@ -641,13 +642,16 @@ func TestPerformedActionRefuses(t *testing.T) { } } ` - for _, name := range []string{"test::C", "test::Rig::nested"} { + for name, want := range map[string]string{ + "test::C": "the body's steps then { y > 5 }", + "test::Rig::nested": "the body's steps then { z > 5 }", + } { refused := refusal(t, src, name) - if refused.Construct != "body statement" { - t.Errorf("%s: refused construct is %q, want the body statement", name, refused.Construct) + if refused.Construct != "constraint body steps" { + t.Errorf("%s: refused construct is %q, want the constraint body steps", name, refused.Construct) } - if refused.Condition != "`perform` statement" { - t.Errorf("%s: refused condition is %q, want the perform statement", name, refused.Condition) + if refused.Condition != want { + t.Errorf("%s: refused condition is %q, want %s", name, refused.Condition, want) } } } @@ -674,14 +678,14 @@ func TestActionFlowRefuses(t *testing.T) { } ` for name, want := range map[string]string{ - "test::C": "`action` statement", - "test::Rig::edge": "`first` statement", - "test::Rig::node": "`action` statement", - "test::Rig::nested": "`action` statement", + "test::C": "the body's steps then { y > 5 }", + "test::Rig::edge": "the body's steps then { z > 5 }", + "test::Rig::node": "the body's steps then { z > 5 }", + "test::Rig::nested": "the body's steps then { z > 5 }", } { refused := refusal(t, src, name) - if refused.Construct != "body statement" { - t.Errorf("%s: refused construct is %q, want the body statement", name, refused.Construct) + if refused.Construct != "constraint body steps" { + t.Errorf("%s: refused construct is %q, want the constraint body steps", name, refused.Construct) } if refused.Condition != want { t.Errorf("%s: refused condition is %q, want %s", name, refused.Condition, want) diff --git a/internal/frontend/grpc/explore_test.go b/internal/frontend/grpc/explore_test.go index 452f7a1488..96f39417fa 100644 --- a/internal/frontend/grpc/explore_test.go +++ b/internal/frontend/grpc/explore_test.go @@ -478,7 +478,7 @@ package Pump { attribute level : Integer = 0; perform action fill : Fill { first start; - then action pour { assign level := level + 1; assign poured := level; } + then action pour { assign level := level + 1; then assign poured := level; } then done; } } diff --git a/internal/ir/lower/action_graph.go b/internal/ir/lower/action_graph.go index f08da87ca6..c580f1643c 100644 --- a/internal/ir/lower/action_graph.go +++ b/internal/ir/lower/action_graph.go @@ -111,6 +111,13 @@ type ActionGraph struct { // keyed by the first statement of the run, whose name names no step. StatementRuns map[ast.Node]bool + // StatementOrders holds order metadata for body lists lowered as part of a + // calculation block's own flow. + StatementOrders map[ast.Node]*StatementOrder + + // UnstatedCaseFlow marks an unordered case body lifted into an action graph. + UnstatedCaseFlow bool + // BlockNodes lists, per node, the action nodes its body's blocks (an `if` branch, // a loop body) declare, in declaration order: subperformances reached by name from it. BlockNodes map[ast.Node][]ast.Node @@ -231,6 +238,7 @@ type Send struct { Message ast.Node Target string TargetSym *symbols.Symbol + Node ast.Node // TargetPath records that Target is a feature chain (`a.b`) reaching through // the sender's features, rather than a name in a namespace (`R`, `P::R`). TargetPath bool @@ -363,6 +371,9 @@ func (DeclareUsage) statement() { /* marker: closed Statement set */ } type Block struct { Statements []Statement Node ast.Node // the loop or branch the block belongs to + // Order is the lowered statement order for a calculation or constraint block. + // Action blocks leave it nil and derive their order from the enclosing graph. + Order *StatementOrder // Scope is the block's own scope, which its declarations, and a loop's // condition, resolve in. Scope *symbols.Scope @@ -1716,7 +1727,7 @@ func lowerStatement(member ast.Node, scope *symbols.Scope) Statement { case *ast.Usage: return lowerUsageStatement(m, scope) default: - return Unsupported{Description: fmt.Sprintf("%T", member), Node: member, Scope: scope} + return Unsupported{Description: statedFlowKeyword(member), Node: member, Scope: scope} } } @@ -1759,6 +1770,7 @@ func lowerSend(m *ast.SendStatement, scope *symbols.Scope) Statement { Message: message, Target: target, TargetSym: targetSym, + Node: m, TargetPath: isPath, TargetExpr: targetExpr, IsVia: m.IsVia, diff --git a/internal/ir/lower/action_starts.go b/internal/ir/lower/action_starts.go index 752cb75160..95eb242077 100644 --- a/internal/ir/lower/action_starts.go +++ b/internal/ir/lower/action_starts.go @@ -60,6 +60,9 @@ func FlowStartError(graph *ActionGraph) error { // the owner's performance (startsConcurrently) nor of the flow it states. A sole // `perform` performs the behavior it names. func performedStep(graph *ActionGraph, node ast.Node) bool { + if graph.StatementRuns[node] { + return true + } usage, ok := node.(*ast.Usage) if !ok || len(graph.Edges[node]) > 0 || usage.IsPerformedAction() { return true @@ -74,7 +77,7 @@ func unorderedSubactions(graph *ActionGraph) []ast.Node { targets := specializedSiblings(graph) var starts []ast.Node for _, node := range graph.Nodes { - if node == graph.Initial || preceded[node] || !startsConcurrently(node) { + if node == graph.Initial || preceded[node] || !graph.StatementRuns[node] && !startsConcurrently(node) { continue } // A sibling subsetting or redefining the node performs it: its performance diff --git a/internal/ir/lower/action_subflow.go b/internal/ir/lower/action_subflow.go index 7ee799182a..5ff6640c57 100644 --- a/internal/ir/lower/action_subflow.go +++ b/internal/ir/lower/action_subflow.go @@ -1,7 +1,7 @@ package lower import ( - "errors" + "slices" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -95,18 +95,19 @@ func lowerActionNode(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope) graph.Subflows[node] = &Subflow{Graph: sub, Err: err} } -// lowerTerminateNode records what a terminate action usage runs: the statements of -// its body as a leaf's, then the terminate it stands for. A body stating a flow of -// its own has no place to end the performance from, so it is refused at initialize. +// 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) { - if graph.Subflows == nil { - graph.Subflows = make(map[ast.Node]*Subflow) - } - graph.Subflows[node] = &Subflow{Err: errors.New("a terminate action usage states no flow of its own")} - return + // The node's parameters and attributes are its own (lowerFeatures), not the block's. + 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)} + } else { + lowerBody(graph, node, scope) } - lowerBody(graph, node, scope) graph.Bodies[node] = append(graph.Bodies[node], lowerStatement(node, scope)) } diff --git a/internal/ir/lower/binding.go b/internal/ir/lower/binding.go index 861c0154ba..f56d57de63 100644 --- a/internal/ir/lower/binding.go +++ b/internal/ir/lower/binding.go @@ -51,6 +51,28 @@ func ToBindings(decl ast.Node, scope *symbols.Scope) []Binding { return out } +// NamespaceBindings lowers the binding connectors a namespace scope owns +// directly. ToBindings reads a declaration's members, which a package or +// namespace body is not, so bindings owned there are collected from the +// scope's own symbols. +func NamespaceBindings(scope *symbols.Scope) []Binding { + if scope == nil { + return nil + } + var out []Binding + scope.ForEachMember(func(sym *symbols.Symbol) bool { + u, ok := sym.Decl.(*ast.Usage) + if !ok || u.Kind != ast.UsageBinding { + return true + } + if binding, ok := lowerBinding(u, scope); ok { + out = append(out, binding) + } + return true + }) + return out +} + func lowerBinding(u *ast.Usage, scope *symbols.Scope) (Binding, bool) { if u == nil { return Binding{}, false diff --git a/internal/ir/lower/block_graph.go b/internal/ir/lower/block_graph.go index adccac772f..57111798cb 100644 --- a/internal/ir/lower/block_graph.go +++ b/internal/ir/lower/block_graph.go @@ -23,6 +23,27 @@ func (block Block) Steps() []Statement { return steps } +// StatementList returns a graph's sequential statement nodes as one list. +func (graph *ActionGraph) StatementList() ([]Statement, bool) { + if graph == nil || len(graph.Nodes) == 0 { + return nil, false + } + var steps []Statement + for _, node := range graph.Nodes { + switch node.(type) { + case *ast.AssignmentActionNode, *ast.IfActionNode, *ast.SendStatement, + *ast.TerminateStatement, *ast.WhileLoopActionNode: + default: + return nil, false + } + if len(graph.Edges[node]) > 1 || len(graph.Bodies[node]) != 1 { + return nil, false + } + steps = append(steps, graph.Bodies[node][0]) + } + return steps, true +} + // blockNeedsFlow reports whether a block's members make it a declaration-order // token flow of its own. func blockNeedsFlow(members []ast.Node) bool { diff --git a/internal/ir/lower/body_moves.go b/internal/ir/lower/body_moves.go new file mode 100644 index 0000000000..b25d4189e5 --- /dev/null +++ b/internal/ir/lower/body_moves.go @@ -0,0 +1,299 @@ +package lower + +import ( + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// A leaf body's performance encloses its start shot, where its initial values and +// inputs are read, and one assignment or other statement performance per statement. +// Another performance may interleave between two of them; BodyDivides says where +// that may change an outcome (docs/internals/design/scheduling.md). + +// ReadsAtStart reports whether a performance of node, beginning, evaluates initial +// values at its start shot before its body runs. +func ReadsAtStart(graph *ActionGraph, node ast.Node) bool { + if len(graph.Bodies[node]) == 0 { + return false + } + for _, feature := range graph.Features[node] { + if feature.Value != nil { + return true + } + } + return false +} + +// BodyDivides reports whether another performance interleaving inside node's +// performance may change an outcome: two or more of the body's moves depend on a +// move another performance may make concurrently. With one such move at most, every +// interleaving reorders only independent moves, so the body runs as one move. +func BodyDivides(graph *ActionGraph, node ast.Node) bool { + moves := bodyMoves(graph, node) + if len(moves) < 2 { + return false + } + root := graph + for root.Enclosing != nil { + root = root.Enclosing + } + if !runsConcurrently(root) { + return false + } + others := concurrentFootprints(root, graph, node) + dependent := 0 + for _, move := range moves { + for _, other := range others { + if move.footprint.Dependent(other) { + dependent += move.times + break + } + } + if dependent > 1 { + return true + } + } + return false +} + +// BodySharesMoves reports whether two or more of node's moves may touch what another +// performance does: alongside moves outside its flow, such a body may be divided by one. +func BodySharesMoves(graph *ActionGraph, node ast.Node) bool { + shared := 0 + for _, move := range bodyMoves(graph, node) { + if touchesShared(move.footprint) { + shared += move.times + } + if shared > 1 { + return true + } + } + return false +} + +// FlowSharesMoves reports whether two or more moves of a performance of graph's flow, +// its start shot and its subflows' moves included, may touch what another performance does; +// the attributes and in parameters the performance holds its own values of are not shared. +func FlowSharesMoves(graph *ActionGraph) bool { + own := ownFeatures(graph) + touches := func(f Footprint) bool { return touchesShared(withoutPlaces(f, own)) } + shared := 0 + b := &footprintBuilder{graph: graph, scope: graph.Scope, declared: declaredFeatures(graph)} + for _, attr := range graph.Attributes { + scope := attr.Scope + if scope == nil { + scope = graph.Scope + } + b.reads(scope, attr.Value) + } + if touches(b.footprint) { + shared++ + } + var walk func(g *ActionGraph) bool + walk = func(g *ActionGraph) bool { + for _, n := range g.Nodes { + if touches(g.Footprints()[n]) { + shared++ + if g.Multiplicities[n] != nil { + shared++ + } + } + if shared > 1 { + return true + } + if sub := g.Subflows[n]; sub != nil && sub.Graph != nil && walk(sub.Graph) { + return true + } + } + return false + } + return walk(graph) +} + +// ownFeatures are the symbols of the attributes and in parameters graph's action declares; +// an output's writes stream to the caller, so outputs are not its own. +func ownFeatures(graph *ActionGraph) map[*symbols.Symbol]bool { + own := make(map[*symbols.Symbol]bool) + for _, attr := range graph.Attributes { + if attr.IsResult || attr.Direction == ast.DirOut || attr.Direction == ast.DirInOut { + continue + } + if sym := featureSymbol(graph.Scope, Feature{Name: attr.Name, Node: attr.Node}); sym != nil { + own[sym] = true + } + } + return own +} + +// withoutPlaces drops from f the places resolving to one of syms. +func withoutPlaces(f Footprint, syms map[*symbols.Symbol]bool) Footprint { + keep := func(places []Place) []Place { + var out []Place + for _, p := range places { + if p.Sym == nil || !syms[p.Sym] { + out = append(out, p) + } + } + return out + } + f.Reads, f.Writes = keep(f.Reads), keep(f.Writes) + return f +} + +// touchesShared reports whether a move may touch what another performance does: a +// feature it holds no pin of, the bus, or a target unresolved. +func touchesShared(f Footprint) bool { + s := sharedOnly(f) + return f.Dynamic || f.messages() || len(s.Reads)+len(s.Writes) > 0 +} + +type bodyMove struct { + footprint Footprint + // times is how many moves it stands for: two for a loop, which may iterate. + times int +} + +// bodyMoves lists the moves a performance of node makes: its start shot, where it +// evaluates initial values, then each statement, a block's or conditional's by its +// own. The start shot's footprint leaves out the pins it seeds that no flow reads. +func bodyMoves(graph *ActionGraph, node ast.Node) []bodyMove { + declared := declaredFeatures(graph) + builder := func() *footprintBuilder { + return &footprintBuilder{graph: graph, node: node, scope: nodeScopeOf(graph, node), declared: declared} + } + var moves []bodyMove + if ReadsAtStart(graph, node) { + b := builder() + for _, f := range graph.Features[node] { + b.reads(f.Scope, f.Value) + if f.Direction == ast.DirOut || f.Direction == ast.DirInOut || f.IsResult { + b.write(Place{Sym: featureSymbol(b.scope, f), Name: f.Name, Local: true}) + } + } + moves = append(moves, bodyMove{footprint: b.footprint, times: 1}) + } + var walk func(stmts []Statement) + walk = func(stmts []Statement) { + for _, stmt := range stmts { + switch s := stmt.(type) { + case Block: + if s.Graph == nil { + walk(s.Statements) + continue + } + case If: + b := builder() + b.reads(s.Scope, s.Condition) + moves = append(moves, bodyMove{footprint: b.footprint, times: 1}) + walk(s.Then.Statements) + if s.Else != nil { + walk(s.Else.Statements) + } + continue + } + b := builder() + b.statement(stmt) + times := 1 + if _, loops := stmt.(Loop); loops { + times = 2 + } + moves = append(moves, bodyMove{footprint: b.footprint, times: times}) + } + } + walk(graph.Bodies[node]) + return moves +} + +// concurrentFootprints lists the footprints of every move of the flow node is in +// that may run concurrently with node's: those of every node of the outermost flow +// and of each flow nested in it, node's own only where it may be performed +// concurrently with itself, held pins aside. +func concurrentFootprints(root, graph *ActionGraph, node ast.Node) []Footprint { + var out []Footprint + var walk func(g *ActionGraph) + walk = func(g *ActionGraph) { + for _, n := range g.Nodes { + footprint := g.Footprints()[n] + if g == graph && n == node { + if !selfConcurrent(graph, node) { + continue + } + footprint = sharedOnly(footprint) + } + out = append(out, footprint) + if sub := g.Subflows[n]; sub != nil && sub.Graph != nil { + walk(sub.Graph) + } + } + } + walk(root) + return out +} + +// selfConcurrent reports whether two performances of node may overlap: it declares +// a multiplicity, or a succession path leads from it back to it. +func selfConcurrent(graph *ActionGraph, node ast.Node) bool { + if graph.Multiplicities[node] != nil { + return true + } + seen := map[ast.Node]bool{} + stack := []ast.Node{node} + for len(stack) > 0 { + n := stack[len(stack)-1] + stack = stack[:len(stack)-1] + for _, edge := range graph.Edges[n] { + if edge.Target == node { + return true + } + if !seen[edge.Target] { + seen[edge.Target] = true + stack = append(stack, edge.Target) + } + } + } + return false +} + +// sharedOnly drops from a footprint the pins each performance holds its own of. +func sharedOnly(f Footprint) Footprint { + keep := func(places []Place) []Place { + var out []Place + for _, p := range places { + if !p.Local { + out = append(out, p) + } + } + return out + } + f.Reads, f.Writes = keep(f.Reads), keep(f.Writes) + return f +} + +// runsConcurrently reports whether any flow under root may hold two tokens at once: +// it forks, starts subactions together, repeats a step or leaves a node by two +// successions other than a decision's. +func runsConcurrently(root *ActionGraph) bool { + if len(root.Concurrent) > 0 { + return true + } + for _, m := range root.Multiplicities { + if m != nil { + return true + } + } + for _, n := range root.Nodes { + switch n.(type) { + case *ast.ForkNode: + return true + case *ast.DecisionNode: + default: + if len(root.Edges[n]) > 1 { + return true + } + } + if sub := root.Subflows[n]; sub != nil && sub.Graph != nil && runsConcurrently(sub.Graph) { + return true + } + } + return false +} diff --git a/internal/ir/lower/calc_body.go b/internal/ir/lower/calc_body.go index cb53efb25a..4a5ba72056 100644 --- a/internal/ir/lower/calc_body.go +++ b/internal/ir/lower/calc_body.go @@ -23,6 +23,12 @@ func CalcBody(owner ast.Node, members []ast.Node, scope *symbols.Scope) []Statem // CalcBodyWith is CalcBody reading the metadata the resolver identifies: a // weighted succession among a case's steps keeps its weight. func CalcBodyWith(owner ast.Node, members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) []Statement { + body, _ := CalcBodyWithOrder(owner, members, scope, resolver) + return body +} + +// CalcBodyWithOrder lowers the body and its calculation-statement order. +func CalcBodyWithOrder(owner ast.Node, members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) ([]Statement, *StatementOrder) { body := make([]ast.Node, 0, len(members)) for _, member := range members { if actual := unwrapMembership(member); actual != nil { @@ -30,12 +36,32 @@ func CalcBodyWith(owner ast.Node, members []ast.Node, scope *symbols.Scope, reso } } if PerformsSteps(owner) && len(flowNodesAmong(body)) > 0 { - return caseSteps(owner, body, scope, resolver) + stmts := caseSteps(owner, body, scope, resolver) + return stmts, nil + } + stmts, order := calcBodyStatements(body, scope, !PerformsSteps(owner)) + if PerformsSteps(owner) { + return stmts, nil } + for i, stmt := range stmts { + stmts[i] = calcStatementOrders(stmt) + } + return stmts, order +} - var stmts, results []Statement +func calcBodyStatements(body []ast.Node, scope *symbols.Scope, ignoreNonStatementSuccessions bool) ([]Statement, *StatementOrder) { + members := make([]ast.Node, 0, len(body)) for _, member := range body { - stmt, ok := calcStep(member, scope) + if actual := unwrapMembership(member); actual != nil && !isAnnotation(actual) { + members = append(members, actual) + } + } + var stmts, results []Statement + for _, member := range members { + if _, ok := member.(*ast.SuccessionEdge); ok { + continue + } + stmt, ok := calcBodyStep(member, scope, ignoreNonStatementSuccessions) if !ok { continue } @@ -45,7 +71,37 @@ func CalcBodyWith(owner ast.Node, members []ast.Node, scope *symbols.Scope, reso } stmts = append(stmts, stmt) } - return append(stmts, results...) + bodyStmts := append(stmts, results...) + order := CalcBodyStatementOrder(scope, members, CalcSteps(bodyStmts)) + return bodyStmts, order +} + +func calcBodyStep(member ast.Node, scope *symbols.Scope, 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) +} + +// CalcSteps returns the statements an invocation performs, excluding bindings +// of output features that are read when their values are needed. +func CalcSteps(body []Statement) []Statement { + steps := make([]Statement, 0, len(body)) + for _, stmt := range body { + ret, ok := stmt.(Return) + if ok && isOutputBinding(ret.Node) { + continue + } + steps = append(steps, stmt) + } + return steps +} + +func isOutputBinding(node ast.Node) bool { + usage, ok := node.(*ast.Usage) + return ok && usage.Direction == ast.DirOut && !usage.IsResult } // calcStep lowers one member of a calculation body and reports whether it @@ -57,14 +113,18 @@ func calcStep(member ast.Node, scope *symbols.Scope) (Statement, bool) { // An input parameter is bound by the invocation, not by the body. return nil, false } + if m.IsSuccessionFlow() || m.Kind == ast.UsageSuccession { + return Unsupported{Description: statedFlowKeyword(member), Node: member, Scope: scope}, true + } return usageStatement(m, scope) case *ast.Definition, *ast.Documentation, *ast.Comment, *ast.Import, *ast.Alias: // Declares a member of the calculation, not a step of it. return nil, false case *ast.SuccessionEdge: - // A calculation body runs its steps in declaration order, so a - // succession states nothing the order does not already state. return nil, false + case *ast.ControlFlowEdge, *ast.InitialNode, *ast.ForkNode, *ast.JoinNode, + *ast.MergeNode, *ast.DecisionNode, *ast.FinalNode: + return Unsupported{Description: statedFlowKeyword(member), Node: member, Scope: scope}, true default: if ast.IsExpression(member) { return Return{Value: member, Node: member, Scope: scope}, true @@ -73,6 +133,156 @@ func calcStep(member ast.Node, scope *symbols.Scope) (Statement, bool) { } } +func calcBlock(block Block, members []ast.Node) Block { + normalized := make([]ast.Node, 0, len(members)) + for _, member := range members { + if actual := unwrapMembership(member); actual != nil && !isAnnotation(actual) { + normalized = append(normalized, actual) + } + } + if block.Graph == nil { + block.Order = CalcBodyStatementOrder(block.Scope, normalized, block.Statements) + for i, stmt := range block.Statements { + block.Statements[i] = calcStatementOrders(stmt) + } + } else { + if steps, ok := block.Graph.StatementList(); ok && block.Stated { + block.Order = CalcBodyStatementOrder(block.Scope, normalized, steps) + } else { + block.Order = CalcBodyStatementOrder(block.Scope, nil, nil) + } + calcGraphStatementOrders(block.Graph) + } + return block +} + +func calcGraphStatementOrders(graph *ActionGraph) { + if graph == nil { + return + } + if graph.StatementOrders == nil { + graph.StatementOrders = make(map[ast.Node]*StatementOrder) + } + for _, node := range graph.Nodes { + stmts := graph.Bodies[node] + members := ast.NodeBodyMembers(node) + if usage, ok := node.(*ast.Usage); ok { + members = usage.Members + } + scope := graph.Scopes[node] + if scope == nil { + scope = graph.Scope + } + if len(stmts) > 0 { + graph.StatementOrders[node] = CalcBodyStatementOrder(scope, members, stmts) + } + for i, stmt := range stmts { + stmts[i] = calcStatementOrders(stmt) + } + graph.Bodies[node] = stmts + if subflow := graph.Subflows[node]; subflow != nil && subflow.Graph != nil { + if steps, ok := subflow.Graph.StatementList(); ok { + graph.StatementOrders[node] = CalcBodyStatementOrder(scope, members, steps) + } + } + } + for _, subflow := range graph.Subflows { + if subflow != nil { + calcGraphStatementOrders(subflow.Graph) + } + } +} + +func calcStatementOrders(stmt Statement) Statement { + switch nested := stmt.(type) { + case If: + if node, ok := nested.Node.(*ast.IfActionNode); ok { + if node.Then != nil { + nested.Then = calcBlock(nested.Then, node.Then.Body) + } + if node.Else != nil && nested.Else != nil { + els := calcBlock(*nested.Else, node.Else.Body) + nested.Else = &els + } + } + return nested + case Loop: + if node, ok := nested.Node.(*ast.WhileLoopActionNode); ok { + nested.Body = calcBlock(nested.Body, node.Body) + } + return nested + case Block: + if node, ok := nested.Node.(*ast.Usage); ok { + return calcBlock(nested, node.Members) + } + } + return stmt +} + +func calcSuccessionEnd(member ast.Node, name *ast.QualifiedName, stmts []Statement, index map[ast.Node]int) (int, bool) { + if member != nil { + node := unwrapMembership(member) + i, ok := index[node] + return i, ok + } + if name == nil || len(name.Parts) == 0 { + return 0, false + } + target := name.Parts[len(name.Parts)-1].Text + for i, stmt := range stmts { + if _, result := stmt.(Return); result { + continue + } + if unsupported, ok := stmt.(Unsupported); ok { + if _, succession := unsupported.Node.(*ast.SuccessionEdge); succession { + continue + } + } + if statementDeclares(statementNode(stmt), target) { + return i, true + } + } + return 0, false +} + +func statementNode(stmt Statement) ast.Node { + switch s := stmt.(type) { + case Assign: + return s.Node + case Block: + return s.Node + case Declare: + return s.Node + case DeclareUsage: + return s.Node + case Effect: + return s.Node + case If: + return s.Node + case Loop: + return s.Node + case Return: + return s.Node + case Send: + return s.Node + case Unsupported: + return s.Node + default: + return nil + } +} + +func statementDeclares(node ast.Node, name string) bool { + switch n := node.(type) { + case *ast.Usage: + actual, _ := ast.EffectiveName(n) + return actual == name + case *ast.InitialNode: + return n.Name() == name + } + return false +} + // usageStatement lowers a usage written in a statement position: a bound result // parameter returns the value it binds, an accept parameter states an effect, // and an attribute declares a value the statements around it read and write. diff --git a/internal/ir/lower/calc_order_test.go b/internal/ir/lower/calc_order_test.go new file mode 100644 index 0000000000..b2ed4cbcb2 --- /dev/null +++ b/internal/ir/lower/calc_order_test.go @@ -0,0 +1,278 @@ +package lower + +import ( + "fmt" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" +) + +func calcOrderFrom(t *testing.T, text string) *StatementOrder { + t.Helper() + p := parser.New(source.New("calc.sysml", []byte(text))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse: %v", p.Diagnostics) + } + for _, member := range root.Members { + def, ok := unwrapMembership(member).(*ast.Definition) + if ok && def.Kind == ast.DefCalc { + body, order := CalcBodyWithOrder(def, def.Members, nil, nil) + if len(CalcSteps(body)) != 5 { + t.Fatalf("calc body has %d steps, want a local, three assignments, and the result", len(CalcSteps(body))) + } + return order + } + } + t.Fatal("no calc definition") + return nil +} + +func TestCalcStatementOrderThenAndUnorderedSteps(t *testing.T) { + order := calcOrderFrom(t, ` + calc def C { + attribute y : Integer := 1; + assign y := y * 2 + 1; + then assign y := y * 3 + 2; + assign y := y + 4; + y + } + `) + got := orders(order) + want := map[string]bool{"01234": true, "01324": true, "03124": true} + if len(got) != len(want) { + t.Fatalf("reached %v, want three admitted orders", got) + } + for _, run := range got { + var key string + for _, step := range run { + key += fmt.Sprint(step) + } + if !want[key] { + t.Errorf("reached %v, which violates succession precedence or is duplicated", run) + } + delete(want, key) + } + if len(want) != 0 { + t.Errorf("did not reach %v", want) + } +} + +func TestCalcNestedStatementOrderThen(t *testing.T) { + p := parser.New(source.New("nested-calc.sysml", []byte(` + calc def C { + attribute y : Integer := 1; + if true { + assign y := y * 2; + then assign y := y + 1; + } + attribute i : Integer := 0; + while i < 1 { + assign y := y * 3; + then assign y := y + 1; + assign i := i + 1; + } + y + } + `))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse: %v", p.Diagnostics) + } + var def *ast.Definition + for _, member := range root.Members { + if candidate, ok := unwrapMembership(member).(*ast.Definition); ok && candidate.Kind == ast.DefCalc { + def = candidate + break + } + } + if def == nil { + t.Fatal("no calc definition") + } + body, _ := CalcBodyWithOrder(def, def.Members, nil, nil) + var ifStmt, loopStmt Statement + for _, stmt := range body { + switch stmt.(type) { + case If: + ifStmt = stmt + case Loop: + loopStmt = stmt + } + } + ifStmtBody, ok := ifStmt.(If) + if !ok || ifStmtBody.Then.Order == nil { + t.Fatalf("if body order = %#v, want lowered order", ifStmtBody.Then.Order) + } + if got := orders(ifStmtBody.Then.Order); len(got) != 1 { + t.Fatalf("if body reaches %v, want its succession to fix the order", got) + } + loop, ok := loopStmt.(Loop) + if !ok || loop.Body.Order == nil { + t.Fatalf("loop body order = %#v, want lowered order", loop.Body.Order) + } + if got := orders(loop.Body.Order); len(got) != 1 { + t.Fatalf("loop body reaches %v, want commuting statements to add no order", got) + } +} + +func TestCalcNestedBlocksCarryTheirOwnPrecedence(t *testing.T) { + p := parser.New(source.New("nested-blocks.sysml", []byte(` + calc def C { + attribute y : Integer := 0; + if true { + assign y := y + 1; + then assign y := y + 2; + } else { + assign y := y + 3; + then assign y := y + 4; + } + while false { + assign y := y + 5; + then assign y := y + 6; + } + for i in 1..2 { + action note { + assign y := y + 7; + then assign y := y + 8; + } + } + for j in 1..2 action note { + assign y := y + 9; + then assign y := y + 10; + } + y + } + `))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse: %v", p.Diagnostics) + } + var def *ast.Definition + for _, member := range root.Members { + if candidate, ok := unwrapMembership(member).(*ast.Definition); ok && candidate.Kind == ast.DefCalc { + def = candidate + break + } + } + if def == nil { + t.Fatal("no calc definition") + } + body, order := CalcBodyWithOrder(def, def.Members, nil, nil) + checked := 0 + assertOrder := func(label string, members []ast.Node, stmts []Statement, order *StatementOrder) { + t.Helper() + if order == nil { + t.Fatalf("%s has no lowered statement order", label) + } + if order.Len() != len(stmts) { + t.Fatalf("%s order has %d entries for %d statements", label, order.Len(), len(stmts)) + } + before, matched := statementPrecedence(members, stmts) + hasSuccession := false + for _, member := range members { + if _, ok := unwrapMembership(member).(*ast.SuccessionEdge); ok { + hasSuccession = true + break + } + } + if hasSuccession && len(matched) == 0 { + t.Fatalf("%s did not match its succession to statements", label) + } + for i := range before { + for j, precedes := range before[i] { + if precedes && !order.before[i][j] { + t.Fatalf("%s order omitted precedence %d before %d", label, i, j) + } + } + } + for _, sequence := range orders(order) { + positions := make(map[int]int, len(sequence)) + for position, statement := range sequence { + positions[statement] = position + } + for i := range order.before { + for j, precedes := range order.before[i] { + if precedes && positions[i] >= positions[j] { + t.Fatalf("%s admits %v, violating statement %d before %d", label, sequence, i, j) + } + } + } + } + checked++ + } + var walkStatements func(string, []Statement) + var walkGraph func(string, *ActionGraph) + walkBlock := func(label string, block Block, members []ast.Node) { + t.Helper() + if block.Order == nil { + t.Fatalf("%s has no lowered statement order", label) + } + if block.Graph == nil { + assertOrder(label, members, block.Statements, block.Order) + } else if steps, ok := block.Graph.StatementList(); ok && block.Stated { + assertOrder(label, members, steps, block.Order) + } + if block.Graph != nil { + walkGraph(label, block.Graph) + return + } + walkStatements(label, block.Statements) + } + walkGraph = func(label string, graph *ActionGraph) { + for _, node := range graph.Nodes { + stmts := graph.Bodies[node] + if len(stmts) > 0 { + nodeMembers := ast.NodeBodyMembers(node) + if usage, ok := node.(*ast.Usage); ok { + nodeMembers = usage.Members + } + assertOrder(label+" action body", nodeMembers, stmts, graph.StatementOrders[node]) + walkStatements(label+" action body", stmts) + } + if subflow := graph.Subflows[node]; subflow != nil && subflow.Graph != nil { + if steps, ok := subflow.Graph.StatementList(); ok { + nodeMembers := ast.NodeBodyMembers(node) + if usage, ok := node.(*ast.Usage); ok { + nodeMembers = usage.Members + } + assertOrder(label+" subflow", nodeMembers, steps, graph.StatementOrders[node]) + } + walkGraph(label+" subflow", subflow.Graph) + } + } + } + walkStatements = func(label string, stmts []Statement) { + for i, stmt := range stmts { + switch nested := stmt.(type) { + case If: + node, ok := nested.Node.(*ast.IfActionNode) + if !ok { + t.Fatalf("%s if has node %T", label, nested.Node) + } + if node.Then != nil { + walkBlock(fmt.Sprintf("%s if %d", label, i), nested.Then, node.Then.Body) + } + if node.Else != nil && nested.Else != nil { + walkBlock(fmt.Sprintf("%s else %d", label, i), *nested.Else, node.Else.Body) + } + case Loop: + node, ok := nested.Node.(*ast.WhileLoopActionNode) + if !ok { + t.Fatalf("%s loop has node %T", label, nested.Node) + } + walkBlock(fmt.Sprintf("%s loop %d", label, i), nested.Body, node.Body) + case Block: + if node, ok := nested.Node.(*ast.Usage); ok { + walkBlock(fmt.Sprintf("%s action %d", label, i), nested, node.Members) + } + } + } + } + assertOrder("calc body", def.Members, body, order) + walkStatements("calc body", body) + if checked < 7 { + t.Fatalf("checked %d statement lists, want each nested list", checked) + } +} diff --git a/internal/ir/lower/case_body.go b/internal/ir/lower/case_body.go index 53f89a5e3e..a0ceab09e3 100644 --- a/internal/ir/lower/case_body.go +++ b/internal/ir/lower/case_body.go @@ -24,34 +24,67 @@ func PerformsSteps(decl ast.Node) bool { } } -// caseSteps lowers a body whose steps are action nodes: the locals it declares, one -// Block over the flow the steps state, then its results — its `return`s, wherever -// declared, and the control flow ending the body that returns on some path -// (trailingResults). A body stating successions or control nodes is the token flow -// an action body is (ToActionGraph); one stating none runs its steps in declaration -// order. The resolver reads the flow's `@Probability` annotations; nil reads none. +// caseSteps lowers a body whose steps are action nodes: its locals, one Block +// over the flow the steps state, then its results. A body stating successions or +// 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) if !statesOwnFlow(body) { - var results []Statement - graph := lowerBlockFlowWith(body, scope, func(graph *ActionGraph, nodes []ast.Node, member ast.Node) (Statement, bool) { - if usage, ok := member.(*ast.Usage); ok { - if stmt, connects := lowerBlockConnector(graph, nodes, usage, scope); connects { - return stmt, stmt != nil - } + trailing = caseTrailingResults(body, scope, trailing) + var locals, results []Statement + var members []ast.Node + statementRuns := make(map[ast.Node]Statement) + flowStarted := false + for _, member := range body { + actual := unwrapMembership(member) + if usage, ok := actual.(*ast.Usage); ok && caseFlowConnector(usage) { + members = append(members, member) + continue + } + if isFlowNode(actual) { + members = append(members, member) + flowStarted = true + continue } stmt, states := calcStep(member, scope) if !states { - return nil, false + members = append(members, member) + continue } - if isReturn(stmt) || trailing[member] { + if isReturn(stmt) || trailing[member] || caseOutputBinding(actual) { results = append(results, stmt) - return nil, false + continue + } + switch declared := stmt.(type) { + case Declare: + usage, _ := actual.(*ast.Usage) + if declared.Value == nil || usage != nil && usage.ValueIsInitial || !flowStarted { + locals = append(locals, stmt) + } else { + statementRuns[actual] = stmt + } + case DeclareUsage: + locals = append(locals, stmt) + default: + members = append(members, member) + flowStarted = true } - return stmt, true - }) - flow := Block{Node: owner, Scope: scope, Graph: graph, Own: true} - return append([]Statement{flow}, results...) + } + graph, err := lowerActionFlow(members, scope, resolver) + if err != nil { + return append(append(locals, Unsupported{ + Description: "the flow the steps of the body state: " + err.Error(), + Node: owner, + Scope: scope, + }), results...) + } + addCasePerformNodes(graph, members, scope) + addCaseStatementRuns(graph, body, statementRuns) + graph.UnstatedCaseFlow = true + StartFlow(graph) + flow := Block{Node: owner, Scope: scope, Graph: graph, Own: true, Stated: true} + return append(append(locals, flow), results...) } // The flow's nodes and the members sequencing them are the graph's; the @@ -100,6 +133,97 @@ func caseSteps(owner ast.Node, body []ast.Node, scope *symbols.Scope, resolver * return append(append(locals, flow), results...) } +func addCasePerformNodes(graph *ActionGraph, members []ast.Node, scope *symbols.Scope) { + for _, member := range members { + node, ok := unwrapMembership(member).(*ast.PerformActionNode) + if !ok { + continue + } + graph.Nodes = append(graph.Nodes, node) + graph.Bodies[node] = []Statement{performEffect(node, scope)} + } +} + +func caseOutputBinding(node ast.Node) bool { + usage, ok := node.(*ast.Usage) + if !ok || usage.Kind != ast.UsageAttribute || usage.Value == nil { + return false + } + for _, relationship := range usage.Relationships { + if relationship != nil && relationship.Kind == ast.RelRedefines { + return true + } + } + return false +} + +func caseTrailingResults(body []ast.Node, scope *symbols.Scope, 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)) { + continue + } + if isFlowNode(unwrapMembership(member)) { + break + } + stmt, states := calcStep(member, scope) + if !states { + continue + } + if declared, ok := stmt.(Declare); ok { + usage, _ := unwrapMembership(member).(*ast.Usage) + if declared.Value != nil && usage != nil && !usage.ValueIsInitial { + trailing[member] = true + continue + } + } + if IsResult(stmt) { + trailing[member] = true + continue + } + break + } + return trailing +} + +func addCaseStatementRuns(graph *ActionGraph, body []ast.Node, runs map[ast.Node]Statement) { + if len(runs) == 0 { + return + } + if graph.StatementRuns == nil { + graph.StatementRuns = make(map[ast.Node]bool) + } + existing := make(map[ast.Node]bool, len(graph.Nodes)) + for _, node := range graph.Nodes { + existing[node] = true + } + var ordered []ast.Node + seen := make(map[ast.Node]bool, len(graph.Nodes)+len(runs)) + for _, member := range body { + node := unwrapMembership(member) + if stmt, ok := runs[node]; ok { + graph.StatementRuns[node] = true + graph.Bodies[node] = []Statement{stmt} + ordered = append(ordered, node) + seen[node] = true + } else if existing[node] && !seen[node] { + ordered = append(ordered, node) + seen[node] = true + } + } + for _, node := range graph.Nodes { + if !seen[node] { + ordered = append(ordered, node) + } + } + graph.Nodes = ordered +} + +func caseFlowConnector(member ast.Node) bool { + usage, ok := member.(*ast.Usage) + return ok && (usage.Kind == ast.UsageBinding || usage.Kind == ast.UsageFlow) +} + // isReturn reports a `return` of a body, a result parameter wherever it is declared. func isReturn(stmt Statement) bool { _, ok := stmt.(Return) @@ -152,6 +276,11 @@ func stepName(node ast.Node) string { // can start a flow stating steps, the graph keeps no start and running it // reports why (FlowStartError). func StartFlow(graph *ActionGraph) { + if graph.UnstatedCaseFlow { + graph.Initial = nil + graph.Concurrent = append([]ast.Node(nil), graph.Nodes...) + return + } if graph.Initial == nil { if start, err := CaseFlowStart(graph); err == nil { graph.Initial = start diff --git a/internal/ir/lower/case_body_test.go b/internal/ir/lower/case_body_test.go index 1329df04bb..eb33f9b40e 100644 --- a/internal/ir/lower/case_body_test.go +++ b/internal/ir/lower/case_body_test.go @@ -114,6 +114,129 @@ func TestCaseBodyEndsWithItsResults(t *testing.T) { return r : Integer = 0; } `) - wantKinds(t, body, "flow", "if", "return") + wantKinds(t, body, "declare", "flow", "if", "return") }) } + +func TestCaseBodyWithoutStatedFlowHasUnorderedSteps(t *testing.T) { + body := caseBodyOf(t, ` + analysis def Unordered { + attribute x : Integer := 1; + action s1 { assign x := x * 10; } + action s2 { assign x := x + 2; } + return : Integer; + x + } + `) + wantKinds(t, body, "declare", "flow", "return") + flow := body[1].(Block) + if !flow.Stated { + t.Fatal("unordered case flow is not executed through its action graph") + } + if !flow.Graph.UnstatedCaseFlow { + t.Fatal("unordered case flow does not retain its case graph metadata") + } + if got := len(flow.Graph.Nodes); got != 2 { + t.Fatalf("flow has %d nodes, want the two action usages", got) + } + if len(flow.Graph.Starts()) != 2 { + t.Fatalf("flow starts at %d nodes, want both unordered action usages", len(flow.Graph.Starts())) + } + for _, node := range flow.Graph.Nodes { + if edges := flow.Graph.Edges[node]; len(edges) != 0 { + t.Errorf("node %v has %d succession edge(s), want no declaration-order chaining", node, len(edges)) + } + } +} + +func TestCaseBodyUnorderedStatementRunsStayInTheActionGraph(t *testing.T) { + body := caseBodyOf(t, ` + analysis def Unordered { + attribute x : Integer := 1; + action s1 { assign x := x * 10; } + attribute y : Integer = x; + action s2 { assign x := x + 2; } + return : Integer; + x + } + `) + wantKinds(t, body, "declare", "flow", "return") + flow := body[1].(Block) + if len(flow.Graph.Nodes) != 3 { + t.Fatalf("flow has %d nodes, want two actions and one statement run", len(flow.Graph.Nodes)) + } + var foundRun bool + for _, node := range flow.Graph.Nodes { + if flow.Graph.StatementRuns[node] { + foundRun = true + if len(flow.Graph.Bodies[node]) != 1 { + t.Fatalf("statement run has %d lowered statements, want one", len(flow.Graph.Bodies[node])) + } + } + if edges := flow.Graph.Edges[node]; len(edges) != 0 { + t.Errorf("node %v has %d succession edge(s), want no declaration-order chaining", node, len(edges)) + } + } + if !foundRun { + t.Fatal("non-initial attribute binding was not lowered as a statement run") + } + if len(flow.Graph.Starts()) != 3 { + t.Fatalf("flow starts at %d nodes, want all three unordered steps", len(flow.Graph.Starts())) + } +} + +func TestActionFlowDoesNotLiftPerformedActionNodes(t *testing.T) { + graph := actionGraphFor(t, `action test { perform action p : P; }`) + for _, node := range graph.Nodes { + if _, ok := node.(*ast.PerformActionNode); ok { + t.Fatal("action flow lifted a perform statement into a graph node") + } + } +} + +func TestCaseBodyUnorderedFlowStartsPerformAndActionNodesTogether(t *testing.T) { + body := caseBodyOf(t, ` + analysis def Unordered { + perform action p : P; + action step : A; + } + `) + wantKinds(t, body, "flow") + flow := body[0].(Block) + if len(flow.Graph.Nodes) != 2 { + t.Fatalf("flow has %d nodes, want the performed action and action usage", len(flow.Graph.Nodes)) + } + if starts := flow.Graph.Starts(); len(starts) != 2 { + t.Fatalf("flow starts at %d nodes, want both unordered steps", len(starts)) + } +} + +func TestCaseBodyTrailingAttributeBindingFollowsTheFlow(t *testing.T) { + body := caseBodyOf(t, ` + analysis def TrailingBinding { + attribute x : Integer := 1; + action step { assign x := 2; } + attribute observed : Integer = x; + return : Integer; + observed + } + `) + wantKinds(t, body, "declare", "flow", "declare", "return") +} + +func TestCaseBodyRedefinedOutputBindingFollowsTheFlow(t *testing.T) { + body := caseBodyOf(t, ` + analysis def OutputBinding { + action first; + attribute :>> observed : Integer = 2; + action second; + return : Integer; + observed + } + `) + wantKinds(t, body, "flow", "declare", "return") + flow := body[0].(Block) + if len(flow.Graph.Nodes) != 2 { + t.Fatalf("flow has %d nodes, want the two action usages", len(flow.Graph.Nodes)) + } +} diff --git a/internal/ir/lower/constraint_body.go b/internal/ir/lower/constraint_body.go new file mode 100644 index 0000000000..b9325e5472 --- /dev/null +++ b/internal/ir/lower/constraint_body.go @@ -0,0 +1,221 @@ +package lower + +import ( + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// ConstraintStep lowers one member of a constraint body to the step it states, +// as calcStep does for a calculation body: statements run in declaration order +// except for precedence stated by successions between steps. false +// for what a constraint body states other ways: the parameters a check binds +// (`in x`), the conditions the body exists to hold (expressions and nested +// constraint, require, assume and assert members), the unnamed bindings a +// 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) { + if actual := unwrapMembership(member); actual != nil { + member = actual + } + switch m := member.(type) { + case *ast.ConstraintMember, *ast.RequireMember, *ast.AssumeMember, *ast.SubjectMember: + // Conditions and the subject declaration of the body, evaluated in the + // state the steps leave — not steps of it. + return nil, false + case *ast.SuccessionEdge, *ast.ControlFlowEdge, *ast.InitialNode, *ast.ForkNode, + *ast.JoinNode, *ast.MergeNode, *ast.DecisionNode, *ast.FinalNode: + return Unsupported{Description: statedFlowKeyword(member), Node: member, Scope: scope}, true + case *ast.WhileLoopActionNode: + return Loop{ + Kind: m.Kind, + Condition: m.Condition, + Until: m.Until, + Variable: m.Variable.Name, + Collection: m.Collection, + Body: constraintLowerBlock(m, m.Body, childScope(scope, m)), + 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)) + lowered.Then = block + } + if m.Else != nil { + block := constraintLowerBlock(m.Else, m.Else.Body, childScope(scope, m.Else)) + 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 + } + if m.Direction == ast.DirIn || m.Direction == ast.DirInOut { + // A parameter is bound by the check, not by the body. + return nil, false + } + if m.IsSuccessionFlow() || m.Kind == ast.UsageSuccession { + return Unsupported{Description: statedFlowKeyword(member), Node: member, Scope: scope}, true + } + if stmt, ok := usageStatement(m, scope); ok { + return stmt, true + } + if m.IsTerminate { + return Effect{Kind: EffectTerminate, Node: m, Scope: scope, Terminates: TerminateEnclosing}, true + } + if m.Kind == ast.UsageAction { + // An action usage states a performance of the action — refused by + // the host as an effect outside the body's own performance. + return performEffect(m, scope), true + } + // An unnamed binding (`:>> limit = 5.0`), an actor or a feature of + // another kind states something the check's environment holds rather + // than a step performing. + return nil, false + case *ast.Definition, *ast.Documentation, *ast.Comment, *ast.Import, *ast.Alias: + return nil, false + default: + if ast.IsExpression(member) { + return nil, false + } + return lowerStatement(member, scope), true + } +} + +func constraintLowerBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope) 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 { + block.Statements = append(block.Statements, stmt) + } + } + return constraintBlockOrder(block, members) +} + +// statedFlowKeyword names the statement a flow member was written with, as the +// refusal to run it in a verdict must name it — a verdict orders steps by +// declaration, not by the successions stated. +func statedFlowKeyword(node ast.Node) string { + switch n := node.(type) { + case *ast.InitialNode: + return "`first` statement" + case *ast.ForkNode: + return "`fork` statement" + case *ast.JoinNode: + return "`join` statement" + case *ast.MergeNode: + return "`merge` statement" + case *ast.DecisionNode: + return "`decide` statement" + case *ast.FinalNode: + return "`done` statement" + case *ast.ControlFlowEdge: + if n.IsElse { + return "`else` succession" + } + return "`if` succession" + case *ast.Usage: + if n.IsSuccessionFlow() { + return "`succession flow` statement" + } + return "`succession` statement" + default: + return "`then` statement" + } +} + +func constraintStatementOrder(scope *symbols.Scope, members []ast.Node, stmts []Statement) *StatementOrder { + before, matched := statementPrecedence(members, stmts) + order := bodyStatementOrder(&ActionGraph{Scope: scope}, nil, stmts, before) + order.skipSuccessions(stmts, matched) + return order +} + +func constraintNestedOrders(stmts []Statement) { + for i, stmt := range stmts { + switch nested := stmt.(type) { + case If: + node, ok := nested.Node.(*ast.IfActionNode) + if !ok { + continue + } + if node.Then != nil { + nested.Then = constraintBlockOrder(nested.Then, node.Then.Body) + } + if node.Else != nil && nested.Else != nil { + els := constraintBlockOrder(*nested.Else, node.Else.Body) + nested.Else = &els + } + stmts[i] = nested + case Loop: + node, ok := nested.Node.(*ast.WhileLoopActionNode) + if !ok { + continue + } + nested.Body = constraintBlockOrder(nested.Body, node.Body) + stmts[i] = nested + case Block: + if node, ok := nested.Node.(*ast.Usage); ok { + stmts[i] = constraintBlockOrder(nested, node.Members) + } + } + } +} + +func constraintBlockOrder(block Block, members []ast.Node) Block { + if block.Graph == nil { + block.Statements = discardMatchedSuccessions(members, block.Statements) + block.Order = constraintStatementOrder(block.Scope, members, block.Statements) + constraintNestedOrders(block.Statements) + return block + } + if steps, ok := block.Graph.StatementList(); ok && block.Stated { + block.Order = constraintStatementOrder(block.Scope, members, steps) + } else { + block.Order = ConstraintBodyStatementOrder(block.Scope, nil) + } + constraintGraphStatementOrders(block.Graph) + return block +} + +func constraintGraphStatementOrders(graph *ActionGraph) { + if graph == nil { + return + } + if graph.StatementOrders == nil { + graph.StatementOrders = make(map[ast.Node]*StatementOrder) + } + for _, node := range graph.Nodes { + stmts := graph.Bodies[node] + nodeMembers := ast.NodeBodyMembers(node) + if usage, ok := node.(*ast.Usage); ok { + nodeMembers = usage.Members + } + scope := graph.Scopes[node] + if scope == nil { + scope = graph.Scope + } + if len(stmts) > 0 { + stmts = discardMatchedSuccessions(nodeMembers, stmts) + graph.Bodies[node] = stmts + graph.StatementOrders[node] = constraintStatementOrder(scope, nodeMembers, stmts) + constraintNestedOrders(stmts) + } + if subflow := graph.Subflows[node]; subflow != nil && subflow.Graph != nil { + if steps, ok := subflow.Graph.StatementList(); ok { + graph.StatementOrders[node] = constraintStatementOrder(scope, nodeMembers, steps) + } + } + } + for _, subflow := range graph.Subflows { + if subflow != nil { + constraintGraphStatementOrders(subflow.Graph) + } + } +} diff --git a/internal/ir/lower/constraint_order_test.go b/internal/ir/lower/constraint_order_test.go new file mode 100644 index 0000000000..f4ef8a8f07 --- /dev/null +++ b/internal/ir/lower/constraint_order_test.go @@ -0,0 +1,161 @@ +package lower + +import ( + "fmt" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" +) + +func TestConstraintBodyStatementOrderThen(t *testing.T) { + p := parser.New(source.New("constraint.sysml", []byte(` + constraint def C { + attribute y : Integer := 1; + assign y := y * 10; + then assign y := y + 2; + y == 12 + } + `))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse: %v", p.Diagnostics) + } + var body []ast.Node + for _, member := range root.Members { + def, ok := unwrapMembership(member).(*ast.Definition) + if ok && def.Kind == ast.DefConstraint { + body = def.Members + break + } + } + if body == nil { + t.Fatal("no constraint definition") + } + var stmts []Statement + var members []ast.Node + for _, member := range body { + if stmt, ok := ConstraintStep(member, nil); ok { + stmts = append(stmts, stmt) + members = append(members, member) + } + } + stmts, order := ConstraintBodyWithOrder(nil, members, stmts) + if len(stmts) != 3 { + t.Fatalf("lowered %d body steps, want the declaration and two assignments", len(stmts)) + } + if order.Len() != len(stmts) || !order.before[1][2] { + t.Fatalf("lowered order = %#v, want assignment 1 before assignment 2", order) + } + if got := orders(order); len(got) != 1 { + t.Fatalf("reached %v, want the stated succession to admit one order", got) + } +} + +func TestConstraintNestedBlocksCarryTheirOwnPrecedence(t *testing.T) { + p := parser.New(source.New("constraint.sysml", []byte(` + constraint def C { + attribute x : Integer := 0; + if true { + assign x := x + 1; + then assign x := x + 2; + } else { + assign x := x + 3; + then assign x := x + 4; + } + while false { + assign x := x + 5; + then assign x := x + 6; + } + for i in 1..2 { + assign x := x + i; + then assign x := x + 1; + } + x >= 0 + } + `))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse: %v", p.Diagnostics) + } + var def *ast.Definition + for _, member := range root.Members { + if candidate, ok := unwrapMembership(member).(*ast.Definition); ok && candidate.Kind == ast.DefConstraint { + def = candidate + break + } + } + if def == nil { + t.Fatal("no constraint definition") + } + var stmts []Statement + for _, member := range def.Members { + if stmt, ok := ConstraintStep(member, nil); ok { + stmts = append(stmts, stmt) + } + } + stmts, order := ConstraintBodyWithOrder(nil, def.Members, stmts) + checked := 0 + var checkedLabels []string + assertOrder := func(label string, members []ast.Node, statements []Statement, got *StatementOrder) { + t.Helper() + if got == nil || got.Len() != len(statements) { + t.Fatalf("%s has order %v for %d statements", label, got, len(statements)) + } + before, matched := statementPrecedence(members, statements) + for i := range before { + for j, precedes := range before[i] { + if precedes && !got.before[i][j] { + t.Fatalf("%s omitted precedence %d before %d", label, i, j) + } + } + } + for _, sequence := range orders(got) { + positions := make(map[int]int, len(sequence)) + for position, statement := range sequence { + positions[statement] = position + } + for i := range got.before { + for j, precedes := range got.before[i] { + if precedes && positions[i] >= positions[j] { + t.Fatalf("%s admits %v, violating %d before %d", label, sequence, i, j) + } + } + } + } + for _, member := range members { + if _, succession := unwrapMembership(member).(*ast.SuccessionEdge); succession && len(matched) == 0 { + t.Fatalf("%s did not match its succession to statements", label) + } + } + checked++ + checkedLabels = append(checkedLabels, label) + } + var walk func(string, []ast.Node, []Statement, *StatementOrder) + walk = func(label string, members []ast.Node, statements []Statement, got *StatementOrder) { + assertOrder(label, members, statements, got) + for i, stmt := range statements { + switch nested := stmt.(type) { + case If: + node := nested.Node.(*ast.IfActionNode) + if node.Then != nil { + walk(fmt.Sprintf("%s if %d", label, i), node.Then.Body, nested.Then.Steps(), nested.Then.Order) + } + if node.Else != nil && nested.Else != nil { + walk(fmt.Sprintf("%s else %d", label, i), node.Else.Body, nested.Else.Steps(), nested.Else.Order) + } + case Loop: + node := nested.Node.(*ast.WhileLoopActionNode) + walk(fmt.Sprintf("%s loop %d", label, i), node.Body, nested.Body.Steps(), nested.Body.Order) + case Block: + node := nested.Node.(*ast.Usage) + walk(fmt.Sprintf("%s action %d", label, i), node.Members, nested.Steps(), nested.Order) + } + } + } + walk("constraint body", def.Members, stmts, order) + if checked < 5 { + t.Fatalf("checked %d statement lists %v, want nested if/else/while/for lists", checked, checkedLabels) + } +} diff --git a/internal/ir/lower/footprint.go b/internal/ir/lower/footprint.go index ea61dc367e..1f337846ba 100644 --- a/internal/ir/lower/footprint.go +++ b/internal/ir/lower/footprint.go @@ -250,11 +250,38 @@ func nodeScopeOf(graph *ActionGraph, node ast.Node) *symbols.Scope { } type footprintBuilder struct { - graph *ActionGraph - node ast.Node - scope *symbols.Scope - declared map[ast.Node]bool - footprint Footprint + graph *ActionGraph + node ast.Node + scope *symbols.Scope + declared map[ast.Node]bool + localDeclarations bool + footprint Footprint + // expanding holds the calc usages whose bindings are being read, so a cyclic binding stops. + expanding map[*ast.Usage]bool +} + +// ConstraintBodyFootprint projects the reads and writes of one constraint +// performance, marking declarations in its body-local scopes as local. +func ConstraintBodyFootprint(scope *symbols.Scope, stmts []Statement) Footprint { + declared := make(map[ast.Node]bool) + var collect func(*symbols.Scope) + collect = func(current *symbols.Scope) { + if current == nil || !current.BodyLocal() { + return + } + for _, member := range current.AllMembers() { + if member != nil && member.Decl != nil { + declared[member.Decl] = true + } + } + for _, child := range current.Children() { + collect(child) + } + } + collect(scope) + builder := &footprintBuilder{scope: scope, declared: declared, localDeclarations: true} + builder.statements(stmts) + return builder.footprint } func (b *footprintBuilder) read(p Place) { @@ -296,6 +323,35 @@ func (b *footprintBuilder) place(scope *symbols.Scope, segments []string, each f each(Place{Sym: sym, Name: redefined, Local: local}) } } + b.calcUsage(sym, usage) + } +} + +// calcUsage adds what reading a calc usage evaluates: the values its members +// bind, and its body, which reads beyond them. +func (b *footprintBuilder) calcUsage(sym *symbols.Symbol, usage *ast.Usage) { + switch usage.Kind { + case ast.UsageCalc, ast.UsageAnalysisCase, ast.UsageVerificationCase: + default: + return + } + b.footprint.Dynamic = true + if b.expanding[usage] { + return + } + if b.expanding == nil { + b.expanding = make(map[*ast.Usage]bool) + } + b.expanding[usage] = true + defer delete(b.expanding, usage) + scope := sym.Scope + if scope == nil { + scope = b.scope + } + for _, member := range usage.Members { + if u, ok := unwrapMembership(member).(*ast.Usage); ok { + b.reads(scope, u.Value) + } } } @@ -468,6 +524,13 @@ func (b *footprintBuilder) statement(stmt Statement) { b.footprint.Sends = append(b.footprint.Sends, Channel{Port: viaPortOf(s)}) case Declare: b.reads(s.Scope, s.Value) + if b.localDeclarations { + if sym, _ := resolve.FeatureSymbolInScope(s.Scope, []string{s.Name}); sym != nil && sym.Decl != nil { + b.declared[sym.Decl] = true + } + b.place(s.Scope, []string{s.Name}, b.write) + return + } b.write(Place{Name: s.Name}) case DeclareUsage: b.footprint.Dynamic = true @@ -489,14 +552,18 @@ func (b *footprintBuilder) statement(stmt Statement) { } case Return: b.reads(s.Scope, s.Value) - if b.graph == nil { - return - } - for _, f := range b.graph.Features[b.node] { - if f.IsResult || f.Direction == ast.DirOut { - b.write(Place{Sym: featureSymbol(b.scope, f), Name: f.Name, Local: true}) + wrote := false + if b.graph != nil { + for _, f := range b.graph.Features[b.node] { + if f.IsResult || f.Direction == ast.DirOut { + b.write(Place{Sym: featureSymbol(b.scope, f), Name: f.Name, Local: true}) + wrote = true + } } } + if !wrote { + b.write(Place{Name: "result"}) + } case Assert: b.assertion(s) case Effect: diff --git a/internal/ir/lower/statement_order.go b/internal/ir/lower/statement_order.go new file mode 100644 index 0000000000..5be84adb09 --- /dev/null +++ b/internal/ir/lower/statement_order.go @@ -0,0 +1,451 @@ +package lower + +import ( + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// StatementOrder is how the statements of one list may run: in runs separated by +// the statements that keep their place, each run's in any order, of which only +// those reordering two dependent statements differ. +type StatementOrder struct { + fixed []bool + // dependent holds for two statements that may not commute; shared, for two + // that both may touch what another performance does. + dependent, shared [][]bool + before [][]bool + skipped []bool +} + +// BodyStatementOrder is the order of stmts, statements of node's body in graph +// or of a block within it. +func BodyStatementOrder(graph *ActionGraph, node ast.Node, stmts []Statement) *StatementOrder { + return bodyStatementOrder(graph, node, stmts, nil) +} + +// CalcBodyStatementOrder is the order of a calculation body's statements, +// including any `then` successions the lowered body states. +func CalcBodyStatementOrder(scope *symbols.Scope, members []ast.Node, stmts []Statement) *StatementOrder { + before, matched := statementPrecedence(members, stmts) + order := bodyStatementOrder(&ActionGraph{Scope: scope}, nil, stmts, before) + order.skipSuccessions(stmts, matched) + return order +} + +func statementPrecedence(members []ast.Node, stmts []Statement) ([][]bool, map[*ast.SuccessionEdge]bool) { + before := make([][]bool, len(stmts)) + for i := range before { + before[i] = make([]bool, len(stmts)) + } + index := make(map[ast.Node]int, len(stmts)) + for i, stmt := range stmts { + if _, result := stmt.(Return); result { + continue + } + if unsupported, ok := stmt.(Unsupported); ok { + if _, succession := unsupported.Node.(*ast.SuccessionEdge); succession { + continue + } + } + if node := statementNode(stmt); node != nil { + index[node] = i + } + } + matched := make(map[*ast.SuccessionEdge]bool) + for _, member := range members { + edge, ok := unwrapMembership(member).(*ast.SuccessionEdge) + if !ok { + continue + } + from, fromOK := calcSuccessionEnd(edge.SourceMember, edge.Source, stmts, index) + to, toOK := calcSuccessionEnd(edge.TargetMember, edge.Target, stmts, index) + if fromOK && toOK && from != to { + before[from][to] = true + matched[edge] = true + } + } + closePrecedence(before) + cycle := false + for i := range before { + if before[i][i] { + cycle = true + break + } + } + if cycle { + for i := range before { + clear(before[i]) + } + clear(matched) + } + return before, matched +} + +// ConstraintBodyStatementOrder is the order of a constraint body's statements. +func ConstraintBodyStatementOrder(scope *symbols.Scope, stmts []Statement) *StatementOrder { + return BodyStatementOrder(&ActionGraph{Scope: scope}, nil, stmts) +} + +// ConstraintBodyWithOrder returns a constraint body's statements with its +// successions between statements recorded as precedence. +func ConstraintBodyWithOrder(scope *symbols.Scope, members []ast.Node, stmts []Statement) ([]Statement, *StatementOrder) { + stmts = discardMatchedSuccessions(members, stmts) + order := constraintStatementOrder(scope, members, stmts) + constraintNestedOrders(stmts) + return stmts, order +} + +func discardMatchedSuccessions(members []ast.Node, stmts []Statement) []Statement { + _, matched := statementPrecedence(members, stmts) + if len(matched) == 0 { + return stmts + } + filtered := make([]Statement, 0, len(stmts)) + for _, stmt := range stmts { + unsupported, ok := stmt.(Unsupported) + edge, succession := unsupported.Node.(*ast.SuccessionEdge) + if ok && succession && matched[edge] { + continue + } + filtered = append(filtered, stmt) + } + return filtered +} + +func bodyStatementOrder(graph *ActionGraph, node ast.Node, stmts []Statement, before [][]bool) *StatementOrder { + declared := declaredFeatures(graph) + n := len(stmts) + o := &StatementOrder{ + fixed: make([]bool, n), + dependent: make([][]bool, n), + shared: make([][]bool, n), + before: cloneMatrix(before, n), + } + footprints := make([]Footprint, n) + chained := chainsStatements(graph, node, stmts) + for i, stmt := range stmts { + o.fixed[i] = chained || keepsPlace(stmt) + b := &footprintBuilder{graph: graph, node: node, scope: nodeScopeOf(graph, node), declared: declared} + b.statement(stmt) + footprints[i] = b.footprint + o.dependent[i], o.shared[i] = make([]bool, n), make([]bool, n) + } + for i := range stmts { + for j := range i { + dep := footprints[i].Dependent(footprints[j]) + if o.before[i][j] || o.before[j][i] { + dep = true + } + shared := touchesShared(footprints[i]) && touchesShared(footprints[j]) + o.dependent[i][j], o.dependent[j][i] = dep, dep + o.shared[i][j], o.shared[j][i] = shared, shared + } + } + return o +} + +// chainsStatements reports whether stmts is the body of an action written as one +// node, whose every statement after the first is a `then` continuation. +func chainsStatements(graph *ActionGraph, node ast.Node, stmts []Statement) bool { + u, ok := node.(*ast.Usage) + if !ok || !u.IsActionNode || graph == nil { + return false + } + body := graph.Bodies[node] + return len(body) > 0 && len(stmts) > 0 && &body[0] == &stmts[0] +} + +// keepsPlace reports whether a statement keeps its place among the others: a +// declaration the statements after it read by name, a `return`, a reference +// performed (`perform`, as no unordered start either), or one the runtime refuses. +func keepsPlace(stmt Statement) bool { + switch s := stmt.(type) { + case Declare, DeclareUsage, Return, Unsupported: + return true + case Effect: + return s.Kind == EffectPerform || s.Kind == EffectStart + } + return false +} + +// Len is how many statements the order is over. +func (o *StatementOrder) Len() int { return len(o.fixed) } + +// Skipped reports whether i is a succession represented by this order, not a +// statement to execute. +func (o *StatementOrder) Skipped(i int) bool { + return i >= 0 && i < len(o.skipped) && o.skipped[i] +} + +// HasSkipped reports whether this order carries any non-executable successions. +func (o *StatementOrder) HasSkipped() bool { + for _, skipped := range o.skipped { + if skipped { + return true + } + } + return false +} + +func (o *StatementOrder) skipSuccessions(stmts []Statement, matched map[*ast.SuccessionEdge]bool) { + o.skipped = make([]bool, len(stmts)) + for i, stmt := range stmts { + unsupported, ok := stmt.(Unsupported) + if !ok { + continue + } + edge, ok := unsupported.Node.(*ast.SuccessionEdge) + o.skipped[i] = ok && matched[edge] + } +} + +// Reorders reports whether two of the statements may run in either order with +// different results: two dependent ones in one run, or with divided set (another +// performance interleaving between statements), two that both touch what it does. +func (o *StatementOrder) Reorders(divided bool) bool { + for i := range o.fixed { + if o.Skipped(i) { + continue + } + for j := i + 1; j < len(o.fixed); j++ { + if !o.Skipped(j) && o.before[j][i] { + return true + } + } + } + for i := range o.fixed { + if o.Skipped(i) { + continue + } + for j := i + 1; j < len(o.fixed); j++ { + if o.Skipped(j) { + continue + } + if o.fixed[j] { + break + } + if !o.fixed[i] && o.dependentPair(i, j, divided) && + !o.before[i][j] && !o.before[j][i] { + return true + } + } + } + return false +} + +// HasReversePrecedence reports whether an explicit succession runs against declaration order. +func (o *StatementOrder) HasReversePrecedence() bool { + for i := range o.fixed { + for j := i + 1; j < len(o.fixed); j++ { + if o.before[j][i] { + return true + } + } + } + return false +} + +func (o *StatementOrder) dependentPair(i, j int, divided bool) bool { + return o.dependent[i][j] || o.before[i][j] || o.before[j][i] || divided && o.shared[i][j] +} + +// run is the statements not done in the run the first of them is in: that one +// alone where it keeps its place. +func (o *StatementOrder) run(done []bool) []int { + var out []int + for i, d := range done { + if d || o.Skipped(i) { + continue + } + if o.fixed[i] { + if len(out) == 0 { + out = append(out, i) + } + return out + } + out = append(out, i) + } + return out +} + +// Next lists, ascending, the statements that may run next after those done, so +// that the statements run in the least order, by position, of every class of +// orders equal up to swapping independent neighbours: each class is reached once. +// A statement blocked waits for one dependent on it to run first. Empty once all are done. +func (o *StatementOrder) Next(done, blocked []bool, divided bool) []int { + remaining := o.run(done) + if len(remaining) < 2 { + return remaining + } + if o.hasPrecedence(remaining) { + var next []int + for _, s := range remaining { + ready := true + for _, predecessor := range remaining { + if o.before[predecessor][s] && !done[predecessor] { + ready = false + break + } + } + if ready { + next = append(next, s) + } + } + for _, s := range next { + independent := true + for _, other := range remaining { + if other != s && (o.dependentPair(s, other, divided) || + o.before[s][other] || o.before[other][s]) { + independent = false + break + } + } + if independent { + return []int{s} + } + } + return next + } + component := o.components(remaining, divided) + var next []int + for _, s := range remaining { + if blocked[s] { + continue + } + // Each statement that must not be a source of the class's order — one before + // s, or blocked — needs a dependent path from one that may run before it. + free := map[int]bool{} + for _, r := range remaining { + if r == s || r > s && !blocked[r] { + free[component[r]] = true + } + } + ok := true + for _, r := range remaining { + if r != s && (r < s || blocked[r]) && !free[component[r]] { + ok = false + break + } + } + if ok { + next = append(next, s) + } + } + return next +} + +// NextFixed lists the declaration-least ready statement, respecting explicit +// precedence without exposing a choice point under a fixed scheduling policy. +func (o *StatementOrder) NextFixed(done, blocked []bool, divided bool) []int { + remaining := o.run(done) + if len(remaining) < 2 { + return remaining + } + if !o.hasPrecedence(remaining) { + return remaining[:1] + } + for _, s := range remaining { + ready := true + for _, predecessor := range remaining { + if o.before[predecessor][s] && !done[predecessor] { + ready = false + break + } + } + if ready { + return []int{s} + } + } + return nil +} + +func (o *StatementOrder) hasPrecedence(remaining []int) bool { + present := make([]bool, len(o.before)) + for _, i := range remaining { + present[i] = true + } + for _, i := range remaining { + for _, j := range remaining { + if o.before[i][j] && present[j] { + return true + } + } + } + return false +} + +// Rivals lists, ascending, the statements not done, besides s, that run with s and may +// not commute with it: those another may run between two moves of s, s once started. +func (o *StatementOrder) Rivals(s int, done []bool, divided bool) []int { + var out []int + for _, r := range o.run(done) { + if r != s && o.dependentPair(r, s, divided) { + out = append(out, r) + } + } + return out +} + +// components numbers the connected components of the dependence among remaining. +func (o *StatementOrder) components(remaining []int, divided bool) map[int]int { + component := make(map[int]int, len(remaining)) + for _, start := range remaining { + if _, seen := component[start]; seen { + continue + } + component[start] = start + stack := []int{start} + for len(stack) > 0 { + i := stack[len(stack)-1] + stack = stack[:len(stack)-1] + for _, j := range remaining { + if _, seen := component[j]; !seen && o.dependentPair(i, j, divided) { + component[j] = start + stack = append(stack, j) + } + } + } + } + return component +} + +// Ran marks s run after those done, unblocking each statement left that depends +// on it and blocking each before it that does not. +func (o *StatementOrder) Ran(s int, done, blocked []bool, divided bool) { + done[s] = true + for r := range done { + if done[r] { + continue + } + switch { + case o.dependentPair(r, s, divided): + blocked[r] = false + case r < s: + blocked[r] = true + } + } +} + +func cloneMatrix(matrix [][]bool, n int) [][]bool { + out := make([][]bool, n) + for i := range out { + out[i] = make([]bool, n) + if i < len(matrix) { + copy(out[i], matrix[i]) + } + } + return out +} + +func closePrecedence(before [][]bool) { + for k := range before { + for i := range before { + if !before[i][k] { + continue + } + for j := range before { + before[i][j] = before[i][j] || before[k][j] + } + } + } +} diff --git a/internal/ir/lower/statement_order_test.go b/internal/ir/lower/statement_order_test.go new file mode 100644 index 0000000000..7f7698e65c --- /dev/null +++ b/internal/ir/lower/statement_order_test.go @@ -0,0 +1,265 @@ +package lower + +import ( + "fmt" + "math/rand/v2" + "slices" + "testing" +) + +// orderOf builds a StatementOrder over n statements from dependent pairs and fixed positions. +func orderOf(n int, dependent [][2]int, fixed ...int) *StatementOrder { + o := &StatementOrder{fixed: make([]bool, n), dependent: make([][]bool, n), shared: make([][]bool, n), before: make([][]bool, n)} + for i := range n { + o.dependent[i], o.shared[i], o.before[i] = make([]bool, n), make([]bool, n), make([]bool, n) + } + for _, p := range dependent { + o.dependent[p[0]][p[1]], o.dependent[p[1]][p[0]] = true, true + } + for _, f := range fixed { + o.fixed[f] = true + } + return o +} + +// orders lists every order the schedule reaches by picking each time among Next. +func orders(o *StatementOrder) [][]int { + var out [][]int + var walk func(done, blocked []bool, prefix []int) + walk = func(done, blocked []bool, prefix []int) { + next := o.Next(done, blocked, false) + if len(next) == 0 { + out = append(out, prefix) + return + } + for _, s := range next { + d, b := slices.Clone(done), slices.Clone(blocked) + o.Ran(s, d, b, false) + walk(d, b, append(slices.Clone(prefix), s)) + } + } + walk(make([]bool, o.Len()), make([]bool, o.Len()), nil) + return out +} + +// class spells the equivalence class of an order: for each dependent pair, which +// ran first, with every pair split by a fixed statement in declaration order. +func class(o *StatementOrder, order []int) string { + pos := make([]int, len(order)) + for k, s := range order { + pos[s] = k + } + key := "" + for i := range order { + for j := i + 1; j < len(order); j++ { + if o.dependent[i][j] || o.fixed[i] || o.fixed[j] { + key += fmt.Sprint(pos[i] < pos[j]) + } + } + } + return key +} + +// admitted lists every order keeping each fixed statement in its place relative to all others. +func admitted(o *StatementOrder) [][]int { + var out [][]int + var perm func(prefix []int, left []int) + perm = func(prefix []int, left []int) { + if len(left) == 0 { + out = append(out, prefix) + return + } + for k, s := range left { + rest := slices.Concat(left[:k:k], left[k+1:]) + ok := true + for _, r := range rest { + if (o.fixed[s] || o.fixed[r]) && r < s { + ok = false + } + } + if ok { + perm(append(slices.Clone(prefix), s), rest) + } + } + } + all := make([]int, o.Len()) + for i := range all { + all[i] = i + } + perm(nil, all) + return out +} + +func TestStatementOrderReachesEachClassOnce(t *testing.T) { + cases := []struct { + name string + order *StatementOrder + wantPaths int + }{ + {"independent", orderOf(3, nil), 1}, + {"one dependent pair", orderOf(2, [][2]int{{0, 1}}), 2}, + {"dependent pair beside an independent one", orderOf(3, [][2]int{{0, 2}}), 2}, + {"chain", orderOf(3, [][2]int{{0, 1}, {1, 2}}), 4}, + {"all dependent", orderOf(3, [][2]int{{0, 1}, {0, 2}, {1, 2}}), 6}, + {"fixed splits runs", orderOf(4, [][2]int{{0, 1}, {2, 3}}, 2), 2}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := orders(c.order) + if len(got) != c.wantPaths { + t.Errorf("reached %d orders %v, want %d", len(got), got, c.wantPaths) + } + checkClasses(t, c.order, got) + }) + } + rng := rand.New(rand.NewPCG(1, 2)) + for trial := range 300 { + n := 2 + rng.IntN(5) + var pairs [][2]int + for i := range n { + for j := i + 1; j < n; j++ { + if rng.IntN(3) == 0 { + pairs = append(pairs, [2]int{i, j}) + } + } + } + var fixed []int + if rng.IntN(4) == 0 { + fixed = append(fixed, rng.IntN(n)) + } + o := orderOf(n, pairs, fixed...) + t.Run(fmt.Sprintf("random %d", trial), func(t *testing.T) { checkClasses(t, o, orders(o)) }) + } +} + +// checkClasses fails unless the orders reached are admitted, one per class, and cover every admitted class. +func checkClasses(t *testing.T, o *StatementOrder, got [][]int) { + t.Helper() + want := map[string]bool{} + for _, order := range admitted(o) { + want[class(o, order)] = true + } + seen := map[string]bool{} + for _, order := range got { + key := class(o, order) + if !want[key] { + t.Errorf("reached %v, which keeps no fixed statement's place", order) + } + if seen[key] { + t.Errorf("reached %v, whose class another order reached", order) + } + seen[key] = true + } + if len(seen) != len(want) { + t.Errorf("reached %d classes, want %d", len(seen), len(want)) + } +} + +func TestStatementOrderReorders(t *testing.T) { + if orderOf(3, nil).Reorders(false) { + t.Error("independent statements reorder") + } + if !orderOf(2, [][2]int{{0, 1}}).Reorders(false) { + t.Error("a dependent pair does not reorder") + } + if orderOf(3, [][2]int{{0, 2}}, 1).Reorders(false) { + t.Error("a pair a fixed statement splits reorders") + } +} + +func TestStatementOrderPrecedenceEnumeratesAllAdmittedOrders(t *testing.T) { + o := orderOf(3, [][2]int{{0, 1}, {0, 2}, {1, 2}}) + o.before[0][1] = true + closePrecedence(o.before) + + got := orders(o) + want := map[string]bool{"012": true, "021": true, "201": true} + if len(got) != len(want) { + t.Fatalf("reached %v, want three admitted orders", got) + } + for _, order := range got { + var key string + for _, s := range order { + key += fmt.Sprint(s) + } + if !want[key] { + t.Errorf("reached %v, which violates precedence or is duplicated", order) + } + delete(want, key) + } + if len(want) != 0 { + t.Errorf("did not reach %v", want) + } + if !o.Reorders(false) { + t.Error("unconstrained dependent pairs do not reorder") + } +} + +func TestStatementOrderPrecedenceCanRemoveAllReordering(t *testing.T) { + o := orderOf(3, [][2]int{{0, 1}, {0, 2}, {1, 2}}) + o.before[0][1], o.before[1][2] = true, true + closePrecedence(o.before) + if o.Reorders(false) { + t.Error("precedence-ordered dependent pairs reorder") + } + o.before[2][0] = true + closePrecedence(o.before) + if !o.Reorders(false) { + t.Error("a precedence edge against declaration order is not reported") + } +} + +func TestStatementOrderPrecedenceStaysWithinRunBeforeFixedStatement(t *testing.T) { + o := orderOf(4, [][2]int{{0, 1}}, 2) + o.before[0][1], o.before[3][0] = true, true + closePrecedence(o.before) + + got := orders(o) + if len(got) != 1 { + t.Fatalf("reached %v, want one fixed-barrier order", got) + } + want := []int{0, 1, 2, 3} + for i, statement := range got[0] { + if statement != want[i] { + t.Fatalf("reached %v, want fixed statement to remain at index 2", got[0]) + } + } +} + +func TestStatementOrderPrecedenceDoesNotAddCommutingAlternatives(t *testing.T) { + o := orderOf(3, [][2]int{{0, 1}}) + o.before[0][1] = true + closePrecedence(o.before) + + got := orders(o) + if len(got) != 1 { + t.Fatalf("reached %v, want one order modulo commuting statements", got) + } +} + +func TestStatementOrderFixedPolicyUsesStableTopologicalOrder(t *testing.T) { + o := orderOf(3, [][2]int{{0, 1}, {0, 2}, {1, 2}}) + o.before[2][0] = true + closePrecedence(o.before) + if !o.HasReversePrecedence() { + t.Fatal("reverse declaration precedence was not detected") + } + + var got []int + done, blocked := make([]bool, o.Len()), make([]bool, o.Len()) + for len(got) < o.Len() { + next := o.NextFixed(done, blocked, false) + if len(next) == 0 { + t.Fatalf("fixed topological order dead-ended after %v", got) + } + statement := next[0] + got = append(got, statement) + o.Ran(statement, done, blocked, false) + } + want := []int{1, 2, 0} + for i, statement := range want { + if got[i] != statement { + t.Fatalf("fixed order = %v, want stable topological order %v", got, want) + } + } +} diff --git a/mkdocs.yml b/mkdocs.yml index c2d597dbe3..da21619463 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -103,6 +103,7 @@ not_in_nav: | project/exact-rational-evaluation.md project/expansion-regions.md project/bitwise-complement.md + project/constraint-body-steps.md project/exception-handlers.md project/pilot-differential.md project/spec-pilot-gap-register.md diff --git a/tests/parser/testdata/parse/action_body_interleavings.golden b/tests/parser/testdata/parse/action_body_interleavings.golden new file mode 100644 index 0000000000..87e6c61a59 --- /dev/null +++ b/tests/parser/testdata/parse/action_body_interleavings.golden @@ -0,0 +1,93 @@ +(RootNamespace + (Membership visibility="default" + (Package name="BodyInterleavings" library=false standard=false + (Import visibility="private" all=false kind=namespace recursive=false imported="ScalarValues" filtered=false) + (Membership visibility="default" + (Definition kind="part" abstract=false variation=false name="Counter" + (Membership visibility="default" + (Usage kind="attribute" name="c" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (LiteralInteger value="0"))))) + (Membership visibility="default" + (Usage kind="part" name="counter" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Counter + (*ast.QualifiedName)))) + (Membership visibility="default" + (Definition kind="action" abstract=false variation=false name="Inc" + (Membership visibility="default" + (Usage kind="attribute" name="t" ref=false direction="none" composite=false derived=false ordered=false nonunique=false initial=true + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (FeatureChainExpr member="c" + (FeatureReference name="counter")))) + (*ast.AssignmentActionNode))) + (Membership visibility="default" + (Definition kind="action" abstract=false variation=false name="Race" + (Membership visibility="default" + (Usage kind="attribute" name="c" ref=false direction="none" composite=false derived=false ordered=false nonunique=false initial=true + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (LiteralInteger value="0"))) + (InitialNode name="start" successor="a") + (Usage kind="action" name="a" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Multiplicity range=false + (LiteralInteger value="2")) + (Membership visibility="default" + (Usage kind="attribute" name="t" ref=false direction="none" composite=false derived=false ordered=false nonunique=false initial=true + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (FeatureReference name="c"))) + (*ast.AssignmentActionNode)) + (FinalNode) + (SuccessionEdge source="a" target="@done"))) + (Membership visibility="default" + (Definition kind="action" abstract=false variation=false name="GuardBranch" + (Membership visibility="default" + (Usage kind="attribute" name="c" ref=false direction="none" composite=false derived=false ordered=false nonunique=false initial=true + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (LiteralInteger value="0"))) + (InitialNode name="start" successor="a") + (Usage kind="action" name="a" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Multiplicity range=false + (LiteralInteger value="2")) + (IfActionNode + (OperatorExpr operator="<" + (FeatureReference name="c") + (LiteralInteger value="1")) + (IfBranchNode kind="then" + (*ast.AssignmentActionNode)))) + (FinalNode) + (SuccessionEdge source="a" target="@done"))) + (Membership visibility="default" + (Definition kind="action" abstract=false variation=false name="Callees" + (InitialNode name="start" successor="") + (ForkNode name="f") + (SuccessionEdge source="start" target="f") + (SuccessionEdge source="f" target="a") + (SuccessionEdge source="f" target="b") + (Membership visibility="default" + (Usage kind="action" name="a" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Inc + (*ast.QualifiedName)))) + (Membership visibility="default" + (Usage kind="action" name="b" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Usage kind="action" name="pb" ref=false direction="none" composite=false derived=false ordered=false nonunique=false prefix="perform" + (Relationship kind="typing" target=Inc + (*ast.QualifiedName))))) + (Membership visibility="default" + (Usage kind="succession" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (ConnectorEnd target="a" + (*ast.QualifiedName)) + (ConnectorEnd target="j" + (*ast.QualifiedName)))) + (Membership visibility="default" + (Usage kind="succession" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (ConnectorEnd target="b" + (*ast.QualifiedName)) + (ConnectorEnd target="j" + (*ast.QualifiedName)))) + (JoinNode name="j") + (FinalNode) + (SuccessionEdge source="j" target="@done")))))) \ No newline at end of file diff --git a/tests/parser/testdata/parse/action_body_interleavings.sysml b/tests/parser/testdata/parse/action_body_interleavings.sysml new file mode 100644 index 0000000000..8b639aa907 --- /dev/null +++ b/tests/parser/testdata/parse/action_body_interleavings.sysml @@ -0,0 +1,49 @@ +package BodyInterleavings { + private import ScalarValues::*; + + part def Counter { + attribute c : Integer = 0; + } + part counter : Counter; + + action def Inc { + attribute t : Integer := counter.c; + assign counter.c := t + 1; + } + + action def Race { + attribute c : Integer := 0; + first start then a; + action a[2] { + attribute t : Integer := c; + assign c := t + 1; + } + then done; + } + + action def GuardBranch { + attribute c : Integer := 0; + first start then a; + action a[2] { + if c < 1 { + assign c := c + 1; + } + } + then done; + } + + action def Callees { + first start; + then fork f; + then a; + then b; + action a : Inc; + action b { + perform action pb : Inc; + } + succession a then j; + succession b then j; + join j; + then done; + } +} diff --git a/tests/parser/testdata/parse/binding_namespace_level.golden b/tests/parser/testdata/parse/binding_namespace_level.golden new file mode 100644 index 0000000000..bdf991c0ed --- /dev/null +++ b/tests/parser/testdata/parse/binding_namespace_level.golden @@ -0,0 +1,58 @@ +(RootNamespace + (Membership visibility="default" + (Package name="P" library=false standard=false + (Membership visibility="default" + (Definition kind="part" abstract=false variation=false name="Car")) + (Membership visibility="default" + (Usage kind="part" name="a" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Car + (*ast.QualifiedName)))) + (Membership visibility="default" + (Usage kind="part" name="b" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Car + (*ast.QualifiedName)))) + (Membership visibility="default" + (Usage kind="binding" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false keyword="bind" + (ConnectorEnd target="a" + (*ast.QualifiedName)) + (ConnectorEnd target="b" + (*ast.QualifiedName)))) + (Membership visibility="default" + (Usage kind="binding" name="named" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (ConnectorEnd target="a" + (*ast.QualifiedName)) + (ConnectorEnd target="b" + (*ast.QualifiedName)))) + (Membership visibility="default" + (Usage kind="part" name="c" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Car + (*ast.QualifiedName)))) + (Membership visibility="default" + (Usage kind="part" name="d" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Car + (*ast.QualifiedName)) + (Membership visibility="default" + (Usage kind="part" name="w2" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Car + (*ast.QualifiedName)))))) + (Membership visibility="default" + (Usage kind="binding" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false keyword="bind" + (ConnectorEnd target="c" + (*ast.QualifiedName)) + (ConnectorEnd target="*ast.FeatureChainExpr" + (FeatureChainExpr member="w2" + (FeatureReference name="d"))))) + (Membership visibility="default" + (Usage kind="binding" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Multiplicity range=false + (LiteralInteger value="1")) + (ConnectorEnd target="a" + (Multiplicity range=true + (LiteralInteger value="0") + (LiteralInteger value="1")) + (*ast.QualifiedName)) + (ConnectorEnd target="b" + (Multiplicity range=true + (LiteralInteger value="0") + (LiteralInteger value="1")) + (*ast.QualifiedName))))))) \ No newline at end of file diff --git a/tests/parser/testdata/parse/binding_namespace_level.sysml b/tests/parser/testdata/parse/binding_namespace_level.sysml new file mode 100644 index 0000000000..776ff185b6 --- /dev/null +++ b/tests/parser/testdata/parse/binding_namespace_level.sysml @@ -0,0 +1,13 @@ +// A binding connector owned by a package: its ends are names and feature chains of +// the namespace's own usages, connector and end multiplicities included. +package P { + part def Car; + part a : Car; + part b : Car; + bind a = b; + binding named bind a = b; + part c : Car; + part d : Car { part w2 : Car; } + bind c = d.w2; + binding [1] bind [0..1] a = [0..1] b; +} diff --git a/tests/parser/testdata/parse/calc_statement_succession.golden b/tests/parser/testdata/parse/calc_statement_succession.golden new file mode 100644 index 0000000000..663e23d152 --- /dev/null +++ b/tests/parser/testdata/parse/calc_statement_succession.golden @@ -0,0 +1,19 @@ +(RootNamespace + (Membership visibility="default" + (Package name="test" library=false standard=false + (Import visibility="private" all=false kind=namespace recursive=false imported="ScalarValues" filtered=false) + (Membership visibility="default" + (Definition kind="calc" abstract=false variation=false name="Ordered" + (Usage kind="attribute" name="" ref=false direction="out" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName))) + (Membership visibility="default" + (Usage kind="attribute" name="y" ref=false direction="none" composite=false derived=false ordered=false nonunique=false initial=true + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (LiteralInteger value="1"))) + (*ast.AssignmentActionNode) + (*ast.AssignmentActionNode) + (SuccessionEdge source="@assign" target="@assign") + (*ast.AssignmentActionNode) + (FeatureReference name="y")))))) \ No newline at end of file diff --git a/tests/parser/testdata/parse/calc_statement_succession.sysml b/tests/parser/testdata/parse/calc_statement_succession.sysml new file mode 100644 index 0000000000..cffb475746 --- /dev/null +++ b/tests/parser/testdata/parse/calc_statement_succession.sysml @@ -0,0 +1,11 @@ +package test { + private import ScalarValues::*; + calc def Ordered { + return : Integer; + attribute y : Integer := 1; + assign y := y * 10; + then assign y := y + 2; + assign y := y + 3; + y + } +} diff --git a/tools/referee/fuml/emit_test.go b/tools/referee/fuml/emit_test.go index 7ff73f6df3..d3a61f4686 100644 --- a/tools/referee/fuml/emit_test.go +++ b/tools/referee/fuml/emit_test.go @@ -403,7 +403,7 @@ func TestExecuteBudgets(t *testing.T) { caller := fixtureActivity(t, s, "Caller") em := emitted(t, s, "Caller") x := executed(caller, []ExpectedOutput{integers("all", 0, 1, 10, 20, 30)}) - ex, err := Execute(context.Background(), em, &x, runtime.ExploreBudget{Runs: 1, Depth: 64}, 1) + ex, err := Execute(context.Background(), em, &x, runtime.ExploreBudget{Runs: 1, Depth: 128}, 1) if err != nil { t.Fatal(err) } diff --git a/tools/referee/pssm/emit.go b/tools/referee/pssm/emit.go index 444777d86b..28f6768648 100644 --- a/tools/referee/pssm/emit.go +++ b/tools/referee/pssm/emit.go @@ -724,7 +724,7 @@ func (e *emitter) plainAction(bh *Behavior, ind, where string) (string, error) { } var b strings.Builder b.WriteString(" {\n") - writeStmts(&b, ind+" ", stmts) + writeSequence(&b, ind+" ", stmts) return b.String() + ind + "}", nil } @@ -822,7 +822,7 @@ func (e *emitter) startTarget(b *strings.Builder, ind string, init *Vertex, tr * return "", err } b.WriteString(" do {\n") - writeStmts(b, ind+" ", stmts) + writeSequence(b, ind+" ", stmts) b.WriteString(ind + "}") } fmt.Fprintf(b, " then %s;\n", target) @@ -947,7 +947,7 @@ func (e *emitter) transition(b *strings.Builder, ind string, t *Transition) erro fmt.Fprintf(b, "%stransition %sfirst %s%s%s", ind, name, source, accept, guard) if len(effect) > 0 { b.WriteString(" do {\n") - writeStmts(b, ind+" ", effect) + writeSequence(b, ind+" ", effect) b.WriteString(ind + "}") } fmt.Fprintf(b, " then %s;\n", target) @@ -1412,7 +1412,7 @@ func (e *emitter) doBody(b *strings.Builder, ind, header string, do *Behavior, w } fmt.Fprintf(b, "%s%s {\n", ind, header) writeStmts(b, ind+" ", params) - writeStmts(b, ind+" ", stmts) + writeSequence(b, ind+" ", stmts) fmt.Fprintf(b, "%s}\n", ind) return nil } @@ -1425,7 +1425,7 @@ func (e *emitter) doBody(b *strings.Builder, ind, header string, do *Behavior, w continue } fmt.Fprintf(b, "%s then action step%d {\n", ind, i+1) - writeStmts(b, ind+" ", s.stmts) + writeSequence(b, ind+" ", s.stmts) fmt.Fprintf(b, "%s }\n", ind) } fmt.Fprintf(b, "%s then done;\n%s}\n", ind, ind) @@ -1509,6 +1509,17 @@ func writeStmts(b *strings.Builder, ind string, stmts []string) { } } +// writeSequence writes a body's statements with `then` between them, the +// order the UML behavior's control flow gives them. +func writeSequence(b *strings.Builder, ind string, stmts []string) { + for i, s := range stmts { + if i > 0 { + s = "then " + s + } + b.WriteString(ind + s + "\n") + } +} + func sortedKeys(m map[string]bool) []string { out := make([]string, 0, len(m)) for k := range m { diff --git a/tools/referee/pssm/emit_behavior.go b/tools/referee/pssm/emit_behavior.go index 45c59d6929..a42d6ed85f 100644 --- a/tools/referee/pssm/emit_behavior.go +++ b/tools/referee/pssm/emit_behavior.go @@ -194,7 +194,7 @@ func (e *emitter) boundEntry(bh *Behavior, ind, where, base string) (string, err var b strings.Builder b.WriteString(" {\n") writeStmts(&b, ind+" ", params) - writeStmts(&b, ind+" ", stmts) + writeSequence(&b, ind+" ", stmts) fmt.Fprintf(&b, "%s}", ind) return b.String(), nil } @@ -386,7 +386,7 @@ func (e *emitter) definition(binding *Binding, where, base string) (string, erro b.WriteString(" first start;\n") if len(stmts) > 0 { b.WriteString(" then action body {\n") - writeStmts(&b, " ", stmts) + writeSequence(&b, " ", stmts) b.WriteString(" }\n") } b.WriteString(" then done;\n }\n") diff --git a/tools/referee/pssm/parameters_test.go b/tools/referee/pssm/parameters_test.go index 67400f8a9b..90da5b590c 100644 --- a/tools/referee/pssm/parameters_test.go +++ b/tools/referee/pssm/parameters_test.go @@ -417,7 +417,7 @@ func TestParametersCallBindsInputsAndReturnsOutputs(t *testing.T) { t.Fatal(err) } for _, want := range []string{ - "accept 'or'(left, right) do {\n assign trigger_v_or_left := left;\n assign trigger_v_or_right := right;\n } then S1;", + "accept 'or'(left, right) do {\n assign trigger_v_or_left := left;\n then assign trigger_v_or_right := right;\n } then S1;", "in left = trigger_v_or_left;", "in right = trigger_v_or_right;", "out result : Boolean;", @@ -637,8 +637,8 @@ func TestParametersInoutBindsOnceAndReturns(t *testing.T) { "inout count : Integer;\n attribute count_written : Integer = 0;\n first start;", "inout count = trigger_bump_count;", "inout count = count;", - "assign count_written := (count + 1);\n assign log :=", - "ToString(count) + \"]\"));\n assign count := count_written;\n", + "assign count_written := (count + 1);\n then assign log :=", + "ToString(count) + \"]\"));\n then assign count := count_written;\n", } { if !strings.Contains(m.Text, want) { t.Errorf("emitted text lacks %q:\n%s", want, m.Text) @@ -664,7 +664,7 @@ func TestParametersInoutWritesReadInputs(t *testing.T) { if err != nil { t.Fatal(err) } - want := "assign b_written := (a + 1);\n assign a_written := (b + 1);\n" + want := "assign b_written := (a + 1);\n then assign a_written := (b + 1);\n" if !strings.Contains(m.Text, want) { t.Errorf("emitted text lacks %q:\n%s", want, m.Text) }