diff --git a/changes/unreleased/body-feature-bindings.fixed.md b/changes/unreleased/body-feature-bindings.fixed.md new file mode 100644 index 0000000000..dbf16dee10 --- /dev/null +++ b/changes/unreleased/body-feature-bindings.fixed.md @@ -0,0 +1 @@ +- **Track `=` values in action, calculation, statement-block, and state-machine bodies.** Their dependencies are re-evaluated when read after a source changes, while `:=`, `default`, and values assigned to a binding retain their existing write behavior. An unsupplied inherited action parameter such as `z = x * 2` now follows a later write to `x` (`action_inherited_default_root`: `z` and `r` change from 6 to 20); the supplied argument in `action_inherited_supplied_parameter` remains held. Body-local calc usages remain evaluated once per declaration. Explore/check now agrees on the shared-writers `=` case with `{3}` rather than `{1,2,3}`. diff --git a/changes/unreleased/case-bindings-unvalued-mark.fixed.md b/changes/unreleased/case-bindings-unvalued-mark.fixed.md new file mode 100644 index 0000000000..65a9812cb3 --- /dev/null +++ b/changes/unreleased/case-bindings-unvalued-mark.fixed.md @@ -0,0 +1,4 @@ +- **A case-local a run left declared without a value stays unvalued in the frame its conditions + read.** `frame.markUnvalued` allocated its map on a copy of the frame, so a mark on a frame that + held no unvalued name yet was lost, and an objective or assertion naming such a local could read + through to a binding of the same name instead of being undecided for the missing value. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index 81abea15ae..30679a9820 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -555,7 +555,7 @@ checked after the result is bound is not a form the runtime offers, and none is | A default reaching through a nested part reads that part's own derived values | `eval.go` `evalFeatureChain` (via `GetFeatureValue`) | `instance_derived_slots.sysml` (`total`) | ✅ Faithful | | A default expression resolves declarations in the scope that declared the feature, while instance feature values take precedence | `shape.go` `EffectiveFeature.DeclScope`, `eval.go` `EvalContext.self` | `instance_derived_slots.sysml` | ✅ Faithful | | Mutually dependent defaults report a cycle rather than recursing to the step budget | `context.go` `derivingSlots`, `errors.go` `ErrCyclicFeatureValue` | `robustness_test.go:cyclic_derived_slot` | ✅ Faithful | -| A value written with `=` holds at all times, not once: a `FeatureValue` with `isDefault = false` binds the feature to its expression's result (KerML 1.0 §7.3.4.5), and a binding requires the same values at both ends (KerML 1.0 §7.4.9), so once a feature the expression read changes, the derived value is derived again from what the object holds now — `attribute a : Integer default 3; attribute d : Integer = a * 2; attribute dd : Integer = d + 1;` reads `d` as `6` and `dd` as `7`, then `18` and `19` after `a` is written `9`, transitively and through a part or a binding the expression read through, and a `default null` collection a classifier's subsetter later supersedes re-derives the roll-up that read it folded. Recomputation is lazy — the write unmaterializes what read the feature, the next read derives it — and only a value nothing assigned tracks: one a run assigned keeps the assignment (`d := 100` then `a := 1` reads `d` as `100`, `dd` as `101`), a probe or transaction that wrote what a derived value read is undone with it, edges and materialization alike, and a value derived from itself is still `ErrCyclicFeatureValue` | `dependents.go` (`deriveFeatureValue` opens the recording frame around the `=` evaluation only, `noteRead` lists the derived value on each feature value it reads and the feature value among what it read, `forgetReads` delists it before it is derived again so a branch it no longer takes cannot unmaterialize it, `beforeWrite`/`afterWrite`/`invalidate` unmaterialize what read a feature that changed, `forgetEdgesOf` drops every edge into or out of an object a failed creation abandons), `instance.go` `SetFeatureValue`, `materializeFeatureValue`, `unfoldSubsettedDefaults`, `binding.go` `assignBindingEndpoint`, `classify.go` `refineFeatureValue`, `subsetting.go` `fillOptionalSubsetters`, `adopt.go` `commit`, `classifier_behavior.go` `forgetBehaviorWrites` | conformance `derived_value_follows_assignment`, `derived_invalidation_test.go:TestDerivedValueRecomputesWhenWhatItReadChanges`, `:TestWrittenDerivedValueKeepsWhatWasAssigned`, `:TestWriteOfTheSameValueLeavesDerivedValues`, `:TestClassifierSubsetterRecomputesWhatReadTheFoldedDefault`, `:TestDerivedValueFollowsAWriteThroughAPartAndABinding`, `:TestProbeRollbackRestoresDerivedValuesAndTheirDependencies`, `:TestProbeRollbackForgetsDependenciesRecordedInIt`, `:TestDerivedValueForgetsTheBranchItNoLongerReads`, `:TestAbandonedObjectsLeaveNoDependencyEdges`, `:TestWriteUnderADerivationDerivesItAgain`, `:TestSelfReferentialDerivedValueStaysACycle`, `:TestNoDependencyIsRecordedOutsideADerivation`, `robustness_test.go:write_into_cyclic_derived_feature_values` | ✅ Faithful (object-level `=` values only: the `in` parameters of a calc or action usage are bound once per invocation and stay bound while its outputs are read — the calc-usage rows under *Calculation (Calc)* above — and a compiled calc reads parameters and library constants alone, so it has no object feature value to track) | +| A value written with `=` holds at all times, not once: a `FeatureValue` with `isDefault = false` binds the feature to its expression's result (KerML 1.0 §7.3.4.5), and a binding requires the same values at both ends (KerML 1.0 §7.4.9), so once a feature the expression read changes, the derived value is derived again from what the object holds now — `attribute a : Integer default 3; attribute d : Integer = a * 2; attribute dd : Integer = d + 1;` reads `d` as `6` and `dd` as `7`, then `18` and `19` after `a` is written `9`, transitively and through a part or a binding the expression read through, and a `default null` collection a classifier's subsetter later supersedes re-derives the roll-up that read it folded. Recomputation is lazy — the write unmaterializes what read the feature, the next read derives it — and only a value nothing assigned tracks: one a run assigned keeps the assignment (`d := 100` then `a := 1` reads `d` as `100`, `dd` as `101`), a probe or transaction that wrote what a derived value read is undone with it, edges and materialization alike, and a value derived from itself is still `ErrCyclicFeatureValue` | `dependents.go` (`deriveFeatureValue` opens the recording frame around the `=` evaluation only, `noteRead` lists the derived value on each feature value it reads and the feature value among what it read, `forgetReads` delists it before it is derived again so a branch it no longer takes cannot unmaterialize it, `beforeWrite`/`afterWrite`/`invalidate` unmaterialize what read a feature that changed, `forgetEdgesOf` drops every edge into or out of an object a failed creation abandons), `instance.go` `SetFeatureValue`, `materializeFeatureValue`, `unfoldSubsettedDefaults`, `binding.go` `assignBindingEndpoint`, `classify.go` `refineFeatureValue`, `subsetting.go` `fillOptionalSubsetters`, `adopt.go` `commit`, `classifier_behavior.go` `forgetBehaviorWrites`; body/performance/state cells use the same dependency graph in `body_bindings.go` and `dependents.go` | conformance `derived_value_follows_assignment`, `derived_invalidation_test.go:TestDerivedValueRecomputesWhenWhatItReadChanges`, `:TestWrittenDerivedValueKeepsWhatWasAssigned`, `:TestWriteOfTheSameValueLeavesDerivedValues`, `:TestClassifierSubsetterRecomputesWhatReadTheFoldedDefault`, `:TestDerivedValueFollowsAWriteThroughAPartAndABinding`, `:TestProbeRollbackRestoresDerivedValuesAndTheirDependencies`, `:TestProbeRollbackForgetsDependenciesRecordedInIt`, `:TestDerivedValueForgetsTheBranchItNoLongerReads`, `:TestAbandonedObjectsLeaveNoDependencyEdges`, `:TestWriteUnderADerivationDerivesItAgain`, `:TestSelfReferentialDerivedValueStaysACycle`, `:TestNoDependencyIsRecordedOutsideADerivation`, `robustness_test.go:write_into_cyclic_derived_feature_values`; body binding fixtures and `TestRuntimeRobustnessBodyFeatureBindings` below | ✅ Faithful for object-level, action, calculation, statement-block and state-machine features; body-local calc usages remain one-shot as noted below | | A default over an undeclared feature fails naming the feature value | `instance.go` `evalFeatureValueDefault` | `robustness_test.go:derived_slot_over_missing_feature` | ✅ Faithful | | 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) | @@ -591,6 +591,49 @@ checked after the result is bound is not a form the runtime offers, and none is | A usage declared with no kind keyword is a reference usage (`doubled = span * 2.0;` inside a `part def`), so it is a feature of the object that materializes and evaluates like any attribute — `Triangle::base { length = Triangle::length; }` values the base's `length` — while the same `name = value;` inside an `enum def` is an enumeration literal and no feature of anything (SysML v2 §7.6 Usages — `DefaultReferenceUsage` in SysML.xtext; §7.7 Enumerations) | `parser/defusage.go` `parseBodyMember` (a bare name is an enumerated value only under `inEnumBody`, an attribute-kinded usage elsewhere), `semantics/shape.go` `IsShapeFeature` | conformance `instance_default_reference_usage` (`beam.doubled` = `8.0`, `beam.end.offset` = `4.0` through `Beam::span`), `instance_library_geometry_triangle` (`tri.base.length` = `4 [m]`) | ✅ Faithful | | What `ShapeItems` declares beyond these rules fails as a typed error, never as an empty collection, a guessed value or a panic: `matingOccurrences` and `spaceBoundary` are Kernel Semantic frame features (`Occurrences.kerml`) and so not features of the object — `ErrNoSuchFeature` — which is also why the `assert constraint { isClosed == vertices->forAll{ … includes(p1.matingOccurrences, p2) } }` bodies of `Path`/`Polygon` are not evaluated as part of materializing a shape; `Cylinder`/`Cone` bind `cf : Surface` whose `edges` are the Kernel `StructuredSpaceObject`'s, so `be`/`ae` are `ErrBindingEnd` (`"cf.edges": feature edges not found`) and `edges = faces.edges`/`vertices = faces.vertices` are `ErrNoSuchFeature` over `cf`, while `base.edges` and `af.edges` — each `Disc`'s own `edges = shape`, related to `be` by the one link `binding [1] bind [0..*] base.edges = [0..*] be` declares — answer the ellipse (the row above), as do `faces` (three), `height` and a `Cylinder` nested as a `Box`'s `voids`; `RightTriangle::hypotenuse.length = (Triangle::length^2 + Triangle::width^2)` is the library's squared length and is reported as the dimension mismatch it is (`ErrTypeMismatch`, `25 [m**2]` into a `LengthValue`) rather than rewritten; an `Ellipse` has one edge and no vertices, both of which evaluate | `runtime/errors.go` `ErrNoSuchFeature` (a member read, chained read or write naming no feature of the object), `runtime/eval.go` `evalFeatureChain`, `runtime/instance.go` `GetFeatureValue`/`SetFeatureValue`, `runtime/library_frame.go` `frameFeature` | `runtime/shape_items_limits_test.go:TestShapeItemsUnsupportedExpressionsAreTypedErrors`, conformance `instance_library_geometry_triangle` (`tri.hypotenuse.length` error), `instance_library_geometry_box` (`box.self`, `box.timeSlices`, `box.startShot` errors) | ⚠️ Approximate (what the library determines from the dimensions — every `Box`/`Rectangle`/`Triangle`/`Ellipse` face, edge and vertex collection not routed through a `[0..1]` group, a `Cylinder`'s/`Cone`'s faces and their edges — evaluates; the vertex-mating constraints, `matingOccurrences`, `spaceBoundary`, the curved face's edge graph through `cf : Surface` and the `[0..1]`-bound groups are the typed errors named here, none an empty collection) | +### Body and Performance Feature Bindings + +The dependency-tracked `=` rule for object features also applies to run-owned +action, calculation, statement-block and state-machine features. A source write +invalidates a tracking cell; its next semantic read derives the value again +through the same dependency graph and declaration checks. `:=` initializes a +writable value once, `default` remains a fallback, and assigning a tracking cell +holds the assigned value and ends its tracking. Supplied action pins and call +arguments are writes. At the end of a performance, a tracking cell derives once +more if it has not materialized and then freezes; this end-of-performance value +is an OpenSysML policy because the language does not specify later reads of an +ended occurrence. State exit invalidates dependents of the exiting state's +attribute cells, so a later read observes the existing inactive-state behavior +rather than a cached value from before exit. +On state re-entry, each frozen unwritten `=` attribute restarts for lazy +rederivation; written values remain held. + +The cells are journaled with their owning maps for snapshot/restore, carried in +held images (tracking values restore unmaterialized and rederive lazily), and +spelled in explore/check keys with both their held value or unset state and +their tracking, written or frozen status. Constraint-body declarations already +reevaluate through `evalNameGeneral`/`declaredValue`. A body-local calc usage +lowered as `DeclareUsage` remains one-shot: its input pins are captured on the +first read in an activation and do not track later changes to body `=` values. + +Implementation: `runtime/body_bindings.go` (`bindBodyCell`, `deriveBodyCell`, +`writeBodyCell`, `freezeBodyCells`), `runtime/dependents.go` (`deriveWith`), +`runtime/held_image_behavior.go`, `runtime/snapshot.go`, and +`runtime/check_state.go`. Tests: conformance `action_body_feature_binding`, +`action_body_feature_occurrence_mirror`, +`action_nested_body_feature_binding`, `calc_body_feature_binding`, +`action_block_body_feature_bindings`, `state_body_feature_binding_change_guard`, +`state_attribute_body_feature_binding`, `constraint_body_feature_binding`, +`action_body_feature_initial_binding`, `action_body_feature_written`, +`action_body_feature_read_only`, `state_behavior_body_feature_bindings`, +`action_step_multiplicity_shared_writers`, `action_inherited_default_root`, +`action_inherited_default_root_fallback`, and +`action_inherited_supplied_parameter`; trace +`action_body_feature_binding.trace.golden`; robustness +`TestRuntimeRobustnessBodyFeatureBindings`. Status: ✅ Faithful for action, +calculation, statement-block and state-machine cells; ⚠️ body-local calc usages +remain one-shot. + ### Redefinition in a Specialization (KerML §7.4.7 Redefinition, SysML v2 §7.6) | Semantic Rule | Implementation | Test Case | Status | @@ -1034,7 +1077,7 @@ re-deriving one from `symbol.Decl` (AGENTS.md §4). | Semantic Rule | Implementation | Test Case | Status | |--------------|----------------|-----------|--------| | An attribute default in a behavior body is evaluated in that body's scope, so a unit an import brought in resolves (`attribute h : LengthValue = 500.0 [m];`) | `lower/action_graph.go` `lowerAttributes` + `ActionGraph.Scope`; `runtime/action_executor.go` `initializeAttributes`; `lower/state_graph.go` + `runtime/state_executor.go` `initializeAttributes` | `action_body_quantity_descent.sysml`, `state_body_quantity_scope.sysml`, `tests/parser/testdata/parse/action_body_quantity_statements.golden` | ✅ Faithful | -| A parameter or attribute default an action inherits from a generalization and does not redefine is seeded when the performance starts, as an owned default is — evaluated in the scope it was declared in, after the owned ones, so a parameter the action redefines is read as redefined (KerML 1.0 §7.4.7: an inherited feature is a feature of the specializing type, with its value); a default that fails to evaluate is reported when the action starts, naming the parameter | `runtime/action_executor.go` `performanceFeatures` (the graph's attributes, then `semantics.Model.MembersOf` less what the library base types contribute) → `initializeAttributes`/`declaresAttribute` | conformance `action_inherited_default_root` (a body reassigning `x` leaves the seeded `z = x * 2` alone), `action_node_inherited_default` (typed, invoked and performed nodes; a redefined `x` read by an inherited `z`); `robustness_test.go:node_inherited_default_that_cannot_be_evaluated` | ✅ Faithful | +| An inherited action parameter is initialized in its declaring scope; an unsupplied `in z = x * 2` binds to that expression for the performance, so a later assignment to `x` rederives `z`, while `default` remains a fallback and a supplied argument or redefinition is kept as a write (KerML 1.0 §7.4.7) | `runtime/action_executor.go` `performanceFeatures`/`initializeAttributes`; `runtime/action_frame.go` `seedPerformance`/`setFrameFeature`; `runtime/body_bindings.go` | conformance `action_inherited_default_root` (unsupplied binding follows `x`), `action_inherited_default_root_fallback` (`default` stays at its initial value), `action_inherited_supplied_parameter` (a supplied argument stays held), `action_node_inherited_default`; `robustness_test.go:node_inherited_default_that_cannot_be_evaluated` | ✅ Faithful | | A statement in a nested action node resolves in *that* node's scope; a statement in a loop body or an `if` branch in the block's own scope | `lower/action_graph.go` `lowerStatement`, `lowerBlock`, `childScope` (`lower/scope.go`); `runtime/action_statements.go` `evalIn` | `action_body_quantity_descent.sysml`, `action_body_shadows_enclosing_scope.sysml` | ✅ Faithful | | A decision guard and an inline expression resolve in the action's own scope, its feature values shadowing it | `runtime/action_executor.go` `stepDecisionNode`, `stepActionExecutionNode` | `action_body_shadows_enclosing_scope.sysml`, `action_control_flow.sysml` | ✅ Faithful | | A transition's guard, effect, time-event duration and change-event condition resolve in the scope the transition was written in; a call trigger's parameters and an accept trigger's payload are members of the transition, reachable by simple name from its guard, effect and body and inaccessible by simple name elsewhere; the exit of the state the transition leaves reads them qualified by the transition's name (row above) | `lower/state_graph.go` `Transition.Scope`/`BodyScope` (via `symbols.TriggerScope`); `symbols/bodyscopes.go` `triggerParameterDefiner`, `payloadParameterDefiner`; `passes/w8c_feature_reference.go`; `resolve/document.go`; `runtime/state_executor.go` `passesGuard`, `scheduleTransitionsForState`, `pollChangeEvents`, `executeAction`; `runtime/state_region_transition.go` `runEffect` | `state_body_quantity_scope.sysml`, `state_call_trigger_guard.sysml`, `state_call_trigger_regions.sysml`, `state_transition_accept_payload.sysml`, `model/behavior_body_resolve_test.go` accept-payload case | ✅ Faithful | diff --git a/internal/exec/runtime/action_executor.go b/internal/exec/runtime/action_executor.go index 5755bdfd31..a6d92c2fb3 100644 --- a/internal/exec/runtime/action_executor.go +++ b/internal/exec/runtime/action_executor.go @@ -301,6 +301,7 @@ func (e *ActionExecutor) performanceFeatures() []lower.Attribute { features = append(features, lower.Attribute{ Name: name, Direction: usage.Direction, IsResult: usage.IsResult, Type: lower.TypeText(usage), Value: value, Node: usage, Scope: scope, Optional: e.ctx.admitsNoValue(member), + Binding: value != nil && !usage.ValueIsInitial && !usage.ValueIsDefault, }) } return features @@ -1313,8 +1314,21 @@ func (e *ActionExecutor) initializeAttributes() error { return fmt.Errorf("%w: read %s of object #%d: %w", ErrActionPerformanceOccurrence, attr.Name, e.occurrence.ID, err) } - if value := fv.HeldValue(); value.Kind != ValInvalid { - e.root.data[e.root.key(attr.Name)] = value + if attr.Binding && !fv.Written && !e.occurrenceRedefinesAttribute(attr, fv) { + value, seeded, err := e.ctx.seedBodyBindingFromFeatureValue(e.root.cells, e.root.key(attr.Name), fv) + if err != nil { + return fmt.Errorf("eval attribute default %s: %w", attr.Name, err) + } + if seeded { + if err := e.streamInitialOutput(attr.Name, value); err != nil { + return err + } + continue + } + } + if value := fv.HeldValue(); value.Kind != ValInvalid && + (!attr.Binding || fv.Written || e.occurrenceRedefinesAttribute(attr, fv)) { + e.ctx.writeBodyValue(e.root.cells, e.root.data, e.root.key(attr.Name), value) if err := e.streamInitialOutput(attr.Name, value); err != nil { return err } @@ -1327,17 +1341,29 @@ func (e *ActionExecutor) initializeAttributes() error { } continue } - value, err := evalDefaults().evalIn(attr.Scope).Eval(attr.Value) + var value Value + var err error + if attr.Binding { + value, err = e.ctx.deriveBodyCell( + e.root.cells, e.root.key(attr.Name), e.root.cells.existingCell(e.root.key(attr.Name)), + ) + } else { + value, err = evalDefaults().evalIn(attr.Scope).Eval(attr.Value) + } if err != nil { return fmt.Errorf("eval attribute default %s: %w", attr.Name, err) } if err := e.holdAttributeAsReal(attr, &value); err != nil { return fmt.Errorf("eval attribute default %s: %w", attr.Name, err) } - if value, err = e.mirrorOccurrence(attr.Name, value); err != nil { - return err + if !attr.Binding { + if value, err = e.mirrorOccurrence(attr.Name, value); err != nil { + return err + } + } + if !attr.Binding { + e.ctx.writeBodyValue(e.root.cells, e.root.data, e.root.key(attr.Name), value) } - e.root.data[e.root.key(attr.Name)] = value if err := e.streamInitialOutput(attr.Name, value); err != nil { return err } @@ -1384,7 +1410,7 @@ func (e *ActionExecutor) bindContextDefault(attr lower.Attribute) bool { if err != nil { return false } - e.root.data[e.root.key(attr.Name)] = value + e.ctx.writeBodyValue(e.root.cells, e.root.data, e.root.key(attr.Name), value) return true } @@ -1430,16 +1456,23 @@ func (e *ActionExecutor) materializeOccurrence() (*Instance, error) { if err != nil { return nil, err } + e.occurrence = inst + e.performances.occurrence = inst for _, attr := range e.features { if value, held := e.root.data[e.root.key(attr.Name)]; held { + if cell := e.root.cells.existingCell(e.root.key(attr.Name)); cell != nil && cell.binding != nil && !cell.fv.Written { + if _, err := e.mirrorBindingOccurrence(attr.Name, value, cell); err != nil { + return nil, fmt.Errorf("%w: seed %s of object #%d: %w", + ErrActionPerformanceOccurrence, attr.Name, inst.ID, err) + } + continue + } if err := inst.BindFeatureValue(e.ctx, attr.Name, value); err != nil { return nil, fmt.Errorf("%w: seed %s of object #%d: %w", ErrActionPerformanceOccurrence, attr.Name, inst.ID, err) } } } - e.occurrence = inst - e.performances.occurrence = inst e.ctx.beginPerformanceLife(inst, e.ctx.newActivation()) return inst, nil } @@ -1477,6 +1510,24 @@ func (e *ActionExecutor) mirrorOccurrence(name string, value Value) (Value, erro return fv.HeldValue(), nil } +// mirrorBindingOccurrence exposes a tracked body feature through its occurrence. +func (e *ActionExecutor) mirrorBindingOccurrence(name string, value Value, cell *bodyCell) (Value, error) { + if e.occurrence == nil || !e.declaresAttribute(name) { + return value, nil + } + mirrored, err := e.ctx.mirrorBodyCell(e.occurrence, name, cell, value) + if err != nil { + return value, fmt.Errorf("%w: write %s of object #%d: %w", + ErrActionPerformanceOccurrence, name, e.occurrence.ID, err) + } + return mirrored, nil +} + +// occurrenceRedefinesAttribute reports whether the occurrence supplies another declaration's default. +func (e *ActionExecutor) occurrenceRedefinesAttribute(attr lower.Attribute, fv *FeatureValue) bool { + return occurrenceHasRedefinedDefault(fv, attr, e.graph.Scope) +} + // setFeature writes into the action's feature space, through the performance // occurrence for a feature the action declares: the occurrence is authoritative // for those, and data mirrors what it holds after the write. @@ -1491,7 +1542,7 @@ func (e *ActionExecutor) setFeature(name string, value Value) error { // rather than by the write to that occurrence. return err } - e.root.data[e.root.key(name)] = value + e.ctx.writeBodyValue(e.root.cells, e.root.data, e.root.key(name), value) e.moved = true return e.streamOutput(name, value) } @@ -1550,19 +1601,27 @@ func (e *ActionExecutor) hasFlow() bool { e.dynamicsKind != lower.NotStateSpace) } +// completeRoot freezes the root bindings and ends its performance. +func (e *ActionExecutor) completeRoot() error { + if err := e.ctx.freezeBodyCells(e.root.cells); err != nil { + return err + } + e.state = StateCompleted + e.endRootPerformance() + return nil +} + // completeWithoutFlow completes an action stating no flow: it performs no step, // so its performance begins, takes its inputs, and ends at once. func (e *ActionExecutor) completeWithoutFlow() error { if err := e.checkResultParameters(); err != nil { return err } - e.ctx.beginPerformanceLife(e.occurrence, e.ctx.newActivation()) + e.beginRootPerformance() if err := e.bindInputs(); err != nil { return err } - e.state = StateCompleted - e.ctx.endPerformanceLife(e.occurrence) - return nil + return e.completeRoot() } // bindInputs writes the supplied inputs into the performance, then the @@ -1583,6 +1642,20 @@ func (e *ActionExecutor) bindInputs() error { return nil } +// beginRootPerformance starts the root performance's dependency lifetime. +func (e *ActionExecutor) beginRootPerformance() { + if e.root.began == 0 { + e.root.began = e.ctx.newActivation() + } + e.ctx.beginPerformanceLife(e.occurrence, e.root.began) +} + +// endRootPerformance ends the root performance's dependency lifetime. +func (e *ActionExecutor) endRootPerformance() { + e.ctx.endPerformanceLife(e.occurrence) + e.ctx.endActivation(e.root.began) +} + type inputBindingError struct{ Err error } func (e inputBindingError) Error() string { return e.Err.Error() } @@ -1699,7 +1772,7 @@ func (e *ActionExecutor) initialize() error { return err } - e.ctx.beginPerformanceLife(e.occurrence, e.ctx.newActivation()) + e.beginRootPerformance() if err := e.bindInputs(); err != nil { return err } @@ -1710,9 +1783,7 @@ func (e *ActionExecutor) initialize() error { if err := checkStreamsReceived(e.root); err != nil { return err } - e.state = StateCompleted - e.ctx.endPerformanceLife(e.occurrence) - return nil + return e.completeRoot() } e.seedTokens(e.root, starts, 0) @@ -2261,8 +2332,7 @@ func (e *ActionExecutor) retireToken(tokenIdx int) error { if err := checkStreamsReceived(e.root); err != nil { return err } - e.state = StateCompleted - e.ctx.endPerformanceLife(e.occurrence) + return e.completeRoot() } return nil } @@ -2600,7 +2670,7 @@ func (e *ActionExecutor) stepNestedAction(tokenIdx int) error { } } - perf, err := e.beginPerformance(token.frame, graph, usage, nil) + perf, err := e.beginPerformance(token.frame, graph, usage, nil, nil) if err != nil { return err } @@ -3319,10 +3389,17 @@ func (e *ActionExecutor) State() ExecutionState { // each nested non-repeated node's latest performance and under `part.attribute` what the one // object each of its own parts denotes holds; a performed usage's mirror its occurrence. func (e *ActionExecutor) Results() map[string]Value { + results, _ := e.ResultsWithError() + return results +} + +// ResultsWithError returns the values the action's features hold and the first binding +// derivation error; a feature whose binding fails to derive is omitted. +func (e *ActionExecutor) ResultsWithError() (map[string]Value, error) { results := make(map[string]Value, len(e.root.data)) - e.root.collect("", results) + err := e.root.collect("", results) e.collectPartsHeld(results) - return results + return results, err } // collectPartsHeld adds the attributes held by the object each part or item usage the diff --git a/internal/exec/runtime/action_frame.go b/internal/exec/runtime/action_frame.go index b71b7338e2..d52fd2307d 100644 --- a/internal/exec/runtime/action_frame.go +++ b/internal/exec/runtime/action_frame.go @@ -91,7 +91,8 @@ type actionFrame struct { body bool // locals are the block-local bindings entered around node in parent's body, // outermost first: a loop variable the node's declarations read. - locals []map[string]Value + locals []map[string]Value + localCells []*bodyCells // outer are the frames around a root performance its bodies read but no performance // holds, outermost first: a state machine's data and its states' attributes. outer []frame @@ -115,7 +116,8 @@ type actionFrame struct { // those of every flow around it, this one's own included. connections []lower.Connection // data holds the values the performance's own features hold. - data map[string]Value + data map[string]Value + cells *bodyCells // features are the parameters and attributes the performance holds, by name. features map[string]ast.FeatureDirection // aliases map each name a held feature redefines to the feature's own name. @@ -241,9 +243,39 @@ func (e *ActionExecutor) newRootFrame() *actionFrame { run: e.ctx.newRun(), } e.declareRootFeatures(root) + e.registerRootBindings(root) return root } +// registerRootBindings installs tracked cells for the root's declared bindings. +func (e *ActionExecutor) registerRootBindings(root *actionFrame) { + for _, attr := range e.features { + if !attr.Binding || attr.Value == nil { + continue + } + scope := attr.Scope + if scope == nil { + scope = root.graph.Scope + } + name := root.key(attr.Name) + cells := e.performances.bodyCells(root) + check := func(value *Value) error { + return e.holdAttributeAsReal(attr, value) + } + var cell *bodyCell + context := func(scope *symbols.Scope) *EvalContext { + return e.performances.evalBindingContext(root, scope, name, root.began) + } + onDerived := func(value *Value) error { + mirrored, err := e.mirrorBindingOccurrence(attr.Name, *value, cell) + *value = mirrored + return err + } + cell = e.ctx.registerBodyBindingInContext(cells, name, attr.Value, scope, check, context, onDerived) + cell.binding.masked = attr.Name + } +} + // declareRootFeatures gives root the attributes the graph declares and the // features the action holds, aliasing what each redefines. func (e *ActionExecutor) declareRootFeatures(root *actionFrame) { @@ -329,7 +361,7 @@ func (f *actionFrame) key(name string) string { // seeding its pins from deliveries, then the arguments it passes its callee, then input bindings, // then its own declared defaults. func (e *performances) beginPerformance( - parent *actionFrame, flow *lower.ActionGraph, node ast.Node, locals []map[string]Value, + parent *actionFrame, flow *lower.ActionGraph, node ast.Node, locals []map[string]Value, localCells []*bodyCells, ) (*actionFrame, error) { perf := &actionFrame{ node: node, @@ -337,6 +369,7 @@ func (e *performances) beginPerformance( scope: flow.Scopes[node], parent: parent, locals: locals, + localCells: localCells, connections: parent.connections, data: make(map[string]Value), features: make(map[string]ast.FeatureDirection), @@ -381,14 +414,29 @@ func (e *performances) beginPerformance( // seedPerformance seeds perf's pins from deliveries, then the arguments it passes its // callee, then input bindings, then its own declared defaults. func (e *performances) seedPerformance(parent *actionFrame, flow *lower.ActionGraph, node ast.Node, perf *actionFrame) error { + activation, endStep := e.ctx.beginStep() + defer endStep() + perf.began = activation + for _, feature := range flow.Features[node] { + if !feature.Binding || feature.Value == nil { + continue + } + check := func(value *Value) error { + return e.ctx.checkBodyDeclaration(feature.Scope, perf.describe(), feature.Name, value) + } + context := func(scope *symbols.Scope) *EvalContext { + return e.evalBindingContext(perf, scope, feature.Name, activation) + } + cell := e.ctx.registerBodyBindingInContext( + e.bodyCells(perf), perf.key(feature.Name), feature.Value, feature.Scope, check, context, nil, + ) + cell.binding.masked = feature.Name + } if err := e.takeDeliveries(parent, node, perf); err != nil { return err } // The arguments, bindings and defaults are one evaluation: a calc usage two of them // read answers once, and another performance evaluates it anew. - activation, endStep := e.ctx.beginStep() - defer endStep() - perf.began = activation if err := e.bindArguments(perf, activation); err != nil { return err } @@ -444,14 +492,37 @@ func (e *performances) seedDeclaredValues(perf *actionFrame, features []lower.Fe } ec := e.evalContextFor(perf, feature.Scope) ec.activation = activation - value, err := ec.Eval(feature.Value) + var value Value + var err error + if feature.Binding { + cells := e.bodyCells(perf) + check := func(value *Value) error { + return e.ctx.checkBodyDeclaration(feature.Scope, perf.describe(), feature.Name, value) + } + name := perf.key(feature.Name) + cell := cells.existingCell(name) + if cell == nil || cell.binding == nil { + context := func(scope *symbols.Scope) *EvalContext { + return e.evalBindingContext(perf, scope, feature.Name, activation) + } + cell = e.ctx.registerBodyBindingInContext( + cells, name, feature.Value, feature.Scope, check, context, nil, + ) + cell.binding.masked = feature.Name + } + value, err = e.ctx.deriveBodyCell(cells, name, cell) + } else { + value, err = ec.Eval(feature.Value) + if err == nil { + err = e.ctx.checkBodyDeclaration(feature.Scope, perf.describe(), feature.Name, &value) + } + if err == nil { + e.ctx.writeBodyValue(perf.cells, perf.data, perf.key(feature.Name), value) + } + } if err != nil { return fmt.Errorf("eval %s of %s: %w", feature.Name, nodeDescription(perf.node), err) } - if err := e.ctx.checkBodyDeclaration(feature.Scope, perf.describe(), feature.Name, &value); err != nil { - return err - } - perf.data[perf.key(feature.Name)] = value if err := e.streamFrom(perf, perf.key(feature.Name), value); err != nil { return err } @@ -463,6 +534,9 @@ func (e *performances) seedDeclaredValues(perf *actionFrame, features []lower.Fe // return to same-named enclosing features, and the bindings at its output pins // carry what it produced to their other ends. func (e *performances) endPerformance(perf *actionFrame) error { + if err := e.ctx.freezeBodyCells(perf.cells); err != nil { + return err + } perf.ended = true if err := checkStreamsReceived(perf); err != nil { return err @@ -644,8 +718,12 @@ func (f *actionFrame) lexicalFrames() []frame { } else { frames = slices.Clone(f.outer) } - for _, local := range f.locals { - frames = append(frames, mapFrame(local)) + for i, local := range f.locals { + fr := mapFrame(local) + if i < len(f.localCells) { + fr.cells = f.localCells[i] + } + frames = append(frames, fr) } return append(frames, performanceFrame(f)) } @@ -762,6 +840,13 @@ func (f *actionFrame) unsupportedRepeatedRead(node ast.Node) error { // holds none reads as the empty sequence, as its declaration does. func (f *actionFrame) pin(name string) (Value, error) { value, ok := f.data[f.key(name)] + if f.perfs != nil && f.perfs.ctx != nil { + var err error + value, ok, err = f.perfs.ctx.readBodyValue(f.cells, f.data, f.key(name)) + if err != nil { + return Value{}, err + } + } if ok { return value, nil } @@ -879,7 +964,7 @@ func (e *performances) takeDeliveries(f *actionFrame, node ast.Node, perf *actio for pin, values := range queues { f.receiveStream(node, pin) f.shiftStaged(node, pin) - perf.data[pin] = values[0] + e.ctx.writeBodyValue(perf.cells, perf.data, pin, values[0]) taken[pin] = values[0] if len(values) == 1 { delete(queues, pin) @@ -953,7 +1038,7 @@ func (e *performances) setFrameFeature(f *actionFrame, name string, value Value) if err := e.ctx.checkNamedWrite(f.scope, f.describe(), name, &value); err != nil { return err } - f.data[f.key(name)] = value + e.ctx.writeBodyValue(f.cells, f.data, f.key(name), value) e.noteFrameWrite(f, name, value) return e.streamFrom(f, f.key(name), value) } @@ -1100,26 +1185,36 @@ func enclosingHolder(perf *actionFrame, name string) (local map[string]Value, ho // lookupEnclosing reads name from the innermost binding around perf that holds // a value for it, the frames around the root included. -func lookupEnclosing(perf *actionFrame, name string) (Value, bool) { +func (e *performances) lookupEnclosing(perf *actionFrame, name string) (Value, bool, error) { for f := perf; f != nil; f = f.parent { for i := len(f.locals) - 1; i >= 0; i-- { - if value, ok := f.locals[i][name]; ok { - return value, true + var cells *bodyCells + if i < len(f.localCells) { + cells = f.localCells[i] + } + if value, ok, err := e.ctx.readBodyValue(cells, f.locals[i], name); err != nil { + return Value{}, false, err + } else if ok { + return value, true, nil } } if f.parent != nil { - if value, ok := f.parent.data[f.parent.key(name)]; ok { - return value, true + if value, ok, err := e.ctx.readBodyValue(f.parent.cells, f.parent.data, f.parent.key(name)); err != nil { + return Value{}, false, err + } else if ok { + return value, true, nil } continue } for i := len(f.outer) - 1; i >= 0; i-- { - if value, ok := f.outer[i].lookup(name); ok { - return value, true + if value, ok, err := f.outer[i].read(e.ctx, name); err != nil { + return Value{}, false, err + } else if ok { + return value, true, nil } } } - return Value{}, false + return Value{}, false, nil } // evalContextFor returns a context evaluating in scope with the performance and @@ -1134,6 +1229,27 @@ func (e *performances) evalContextFor(perf *actionFrame, scope *symbols.Scope) * return ec } +// evalBindingContext resolves a binding in its performance while masking its own name. +func (e *performances) evalBindingContext( + perf *actionFrame, scope *symbols.Scope, name string, activation int64, +) *EvalContext { + ec := e.evalContextFor(perf, scope) + ec.activation = activation + if perf == nil { + return ec + } + for i := len(ec.frames) - 1; i >= 0; i-- { + if ec.frames[i].perf != perf { + continue + } + f := ec.frames[i] + f.masked = map[string]bool{perf.key(name): true} + ec.frames[i] = f + break + } + return ec +} + // evalContextAround returns a context evaluating in scope what is written at perf's // node: the enclosing performances and the block-locals around the node, not perf's own. // What is written there is a statement of the body, reaching the performer's features @@ -1148,8 +1264,12 @@ func (e *performances) evalContextAround(perf *actionFrame, scope *symbols.Scope ec.pushFrame(f) } } - for _, local := range perf.locals { - ec.pushFrame(mapFrame(local)) + for i, local := range perf.locals { + fr := mapFrame(local) + if i < len(perf.localCells) { + fr.cells = perf.localCells[i] + } + ec.pushFrame(fr) } return ec } @@ -1207,16 +1327,31 @@ func lexicalValues(perf *actionFrame) map[string]Value { // collect reports the values the performance and its non-repeated subactions hold, // under their paths (`p.v`), the latest performance of each name standing for it. -func (f *actionFrame) collect(prefix string, into map[string]Value) { +func (f *actionFrame) collect(prefix string, into map[string]Value) error { + var deriveErr error + if f.perfs != nil && f.perfs.ctx != nil { + deriveErr = f.perfs.ctx.deriveBodyCells(f.cells) + } for name, value := range f.data { + var cell *bodyCell + if f.cells != nil { + cell = f.cells.cells[name] + } + if cell != nil && cell.binding != nil && + !cell.fv.Written && !cell.binding.frozen && !cell.fv.Materialized { + continue + } into[prefix+name] = value } for name, sub := range f.latestSubactions() { if sub.repetition > 0 { continue } - sub.collect(prefix+name+".", into) + if err := sub.collect(prefix+name+".", into); deriveErr == nil { + deriveErr = err + } } + return deriveErr } // latestSubactions is the latest performance of each named node under f, by name. @@ -1356,7 +1491,10 @@ func boundEndError(end boundEnd, err error) error { // outputPinValue is the value a pin of direction dir carries to the other end of // its binding, carried false when the pin holds nothing the binding moves. func (e *performances) outputPinValue(perf *actionFrame, end boundEnd, dir ast.FeatureDirection) (Value, bool, error) { - value, ok := perf.data[perf.key(end.Pin)] + value, ok, err := e.ctx.readBodyValue(perf.cells, perf.data, perf.key(end.Pin)) + if err != nil { + return Value{}, false, err + } switch dir { case ast.DirOut, ast.DirInOut: if !ok { @@ -1371,7 +1509,9 @@ func (e *performances) outputPinValue(perf *actionFrame, end boundEnd, dir ast.F if !ok { return Value{}, false, nil } - if other, held := e.otherEndHeld(perf, end); held && e.ctx.equalValues(other, value) { + if other, held, err := e.otherEndHeld(perf, end); err != nil { + return Value{}, false, err + } else if held && e.ctx.equalValues(other, value) { return Value{}, false, nil } return value, true, nil @@ -1549,23 +1689,22 @@ func (e *performances) bindingOtherValue(perf *actionFrame, end boundEnd, activa // otherEndHeld reads what the other end of a binding holds now: a performed node's // pin, an enclosing feature named outright, or the feature a chain reaches. -func (e *performances) otherEndHeld(perf *actionFrame, end boundEnd) (Value, bool) { +func (e *performances) otherEndHeld(perf *actionFrame, end boundEnd) (Value, bool, error) { if end.OtherNode != nil { other, performed := e.otherPerformance(perf, end) if !performed { - return Value{}, false + return Value{}, false, nil } - value, held := other.data[other.key(end.OtherPin)] - return value, held + return e.ctx.readBodyValue(other.cells, other.data, other.key(end.OtherPin)) } if name := simpleEndName(end.Other); name != "" { return e.bindingEndContext(end).Lookup(name) } if end.OtherChain != nil { value, err := e.bindingEndContext(end).Eval(end.Other) - return value, err == nil + return value, err == nil, err } - return Value{}, false + return Value{}, false, nil } // unheldEnd reports whether err, from reading the other end of a binding, says that end @@ -1579,7 +1718,10 @@ func (e *performances) unheldEnd(end boundEnd, err error) bool { if end.OtherNode != nil || name == "" { return false } - _, valued := e.bindingEndContext(end).Lookup(name) + _, valued, lookupErr := e.bindingEndContext(end).Lookup(name) + if lookupErr != nil { + return false + } _, _, holds := enclosingHolder(end.at, name) return !valued && holds } @@ -1670,7 +1812,9 @@ func (e *performances) beginInvocation(perf *actionFrame, inv actionInvocation) in, out := parameterNames(params) inputs := make(map[string]Value, len(in)) for _, name := range in { - if value, ok := perf.data[perf.key(name)]; ok { + if value, ok, err := e.ctx.readBodyValue(perf.cells, perf.data, perf.key(name)); err != nil { + return nil, err + } else if ok { inputs[name] = value } } @@ -1682,7 +1826,9 @@ func (e *performances) beginInvocation(perf *actionFrame, inv actionInvocation) return nil, err } } - e.bindPerformerInputs(perf, inv, params, in, inputs, performer) + if err := e.bindPerformerInputs(perf, inv, params, in, inputs, performer); err != nil { + return nil, err + } if err := checkInputsBound(inv, params, inputs); err != nil { return nil, err } @@ -1702,7 +1848,7 @@ func (e *performances) beginInvocation(perf *actionFrame, inv actionInvocation) // default resolved on it, which bindContextDefault resolves once it runs on // that object; a like-named feature of the caller is the argument only for a // `ref` input the performer cannot supply. -func (e *performances) bindPerformerInputs(perf *actionFrame, inv actionInvocation, params []actionParameter, in []string, inputs map[string]Value, performer *Instance) { +func (e *performances) bindPerformerInputs(perf *actionFrame, inv actionInvocation, params []actionParameter, in []string, inputs map[string]Value, performer *Instance) error { if inv.expr == nil { refInputs := make(map[string]bool) if inv.chain != nil { @@ -1716,13 +1862,15 @@ func (e *performances) bindPerformerInputs(perf *actionFrame, inv actionInvocati if _, bound := inputs[name]; bound || refInputs[name] { continue } - if value, ok := lookupEnclosing(perf, name); ok { + if value, ok, err := e.lookupEnclosing(perf, name); err != nil { + return err + } else if ok { inputs[name] = value } } } if inv.chain == nil { - return + return nil } for _, param := range params { if param.IsReference { @@ -1731,6 +1879,7 @@ func (e *performances) bindPerformerInputs(perf *actionFrame, inv actionInvocati } } } + return nil } // streamCalleeOutput is what the action a node performs writes its outputs through @@ -1745,7 +1894,7 @@ func (e *performances) streamCalleeOutput(perf *actionFrame, out []string) func( if !outputs[key] { return nil } - perf.data[key] = value + e.ctx.writeBodyValue(perf.cells, perf.data, key, value) return e.streamFrom(perf, key, value) } } @@ -1754,7 +1903,7 @@ func (e *performances) streamCalleeOutput(perf *actionFrame, out []string) func( // own: its features' values, and its subactions, read as `call.inner.v`. func (f *actionFrame) adopt(callee *ActionExecutor) { for name, value := range callee.root.data { - f.data[f.key(name)] = value + callee.ctx.writeBodyValue(f.cells, f.data, f.key(name), value) } f.performs = callee.graph if len(callee.root.subactions) > 0 && f.subactions == nil { @@ -1785,8 +1934,9 @@ func checkInputsBound(inv actionInvocation, params []actionParameter, inputs map // performanceFrame is the frame an evaluation reads a performance's values // through, which also answers for the nodes of its flow. func performanceFrame(f *actionFrame) frame { - fr := frame{vars: f.data, aliases: f.aliases, perf: f, run: f.run} + fr := frame{vars: f.data, aliases: f.aliases, perf: f, run: f.run, cells: f.cells} if f.perfs != nil { + fr.ensureCells = func() *bodyCells { return f.perfs.bodyCells(f) } // A qualified write lands on the run's own path: the declaration check, // the performance occurrence, and the flows streaming the written pin. fr.write = func(_ frame, name string, value Value) error { @@ -1796,6 +1946,16 @@ func performanceFrame(f *actionFrame) frame { return fr } +// bodyCells lazily gives a performance data map its dependency cells. +func (e *performances) bodyCells(perf *actionFrame) *bodyCells { + if perf.cells == nil { + perf.cells = newBodyCells(perf.data, func(scope *symbols.Scope) *EvalContext { + return e.evalContextFor(perf, scope) + }) + } + return perf.cells +} + // cloneUnreceived copies the unreceived streams of a frame, queues included. func cloneUnreceived(m map[ast.Node][]unreceivedStream) map[ast.Node][]unreceivedStream { if m == nil { diff --git a/internal/exec/runtime/action_statements.go b/internal/exec/runtime/action_statements.go index 33d42358a2..9286468944 100644 --- a/internal/exec/runtime/action_statements.go +++ b/internal/exec/runtime/action_statements.go @@ -2,7 +2,6 @@ package runtime import ( "fmt" - "slices" "sort" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" @@ -199,7 +198,11 @@ func (h *actionStmtHost) effect(engine *stmtEngine, s lower.Effect) error { } // The performed action reads the values in scope where it is performed and its // outputs come back to them, so a perform in a loop body sees that iteration. - _, outputs, err := invokeAction(h.exec.ctx, s.Scope, inv, env.values(), h.exec.self) + values, err := env.values(h.exec.ctx) + if err != nil { + return fmt.Errorf("%s: %w", h.describe(), err) + } + _, outputs, err := invokeAction(h.exec.ctx, s.Scope, inv, values, h.exec.self) if err != nil { return fmt.Errorf("%s: %w", h.describe(), err) } @@ -209,7 +212,7 @@ func (h *actionStmtHost) effect(engine *stmtEngine, s lower.Effect) error { } sort.Strings(names) for _, name := range names { - if env.assign(name, outputs[name]) { + if env.assign(h.exec.ctx, name, outputs[name]) { continue } if written, err := h.exec.returnEnclosing(h.perf, name, outputs[name], h.exec.owner.assignAround); written || err != nil { @@ -218,7 +221,7 @@ func (h *actionStmtHost) effect(engine *stmtEngine, s lower.Effect) error { } continue } - env.data.set(name, outputs[name]) + env.data.setBody(h.exec.ctx, name, outputs[name]) } return nil } @@ -258,7 +261,10 @@ func (e *performances) performNode(parent *actionFrame, engine *stmtEngine, grap } } if f.perf == nil { - if f.perf, err = e.beginPerformance(parent, graph, node, slices.Clone(engine.env.frames)); err != nil { + locals, localCells := engine.env.localFrames() + if f.perf, err = e.beginPerformance( + parent, graph, node, locals, localCells, + ); err != nil { return flowNext, err } } @@ -352,13 +358,15 @@ func (e *performances) performBlockFlow(parent *actionFrame, engine *stmtEngine, if scope == nil { scope = parent.scope } + locals, localCells := engine.env.localFrames() f.perf = &actionFrame{ node: block.Node, graph: block.Graph, flow: block.Graph, scope: scope, parent: parent, - locals: slices.Clone(engine.env.frames), + locals: locals, + localCells: localCells, connections: joinConnections(parent.connections, block.Graph.Connections), data: make(map[string]Value), features: make(map[string]ast.FeatureDirection), @@ -378,7 +386,7 @@ func (e *performances) performBlockFlow(parent *actionFrame, engine *stmtEngine, for _, attr := range block.Graph.Attributes { f.perf.features[attr.Name] = attr.Direction features = append(features, lower.Feature{ - Name: attr.Name, Direction: attr.Direction, IsResult: attr.IsResult, + Name: attr.Name, Direction: attr.Direction, IsResult: attr.IsResult, Binding: attr.Binding, Value: attr.Value, Node: attr.Node, Scope: block.Scope, }) } diff --git a/internal/exec/runtime/action_terminate.go b/internal/exec/runtime/action_terminate.go index 0d142eda61..38888b05ee 100644 --- a/internal/exec/runtime/action_terminate.go +++ b/internal/exec/runtime/action_terminate.go @@ -226,7 +226,7 @@ func (e *ActionExecutor) beginPending(parent *actionFrame, node ast.Node) ([]*ac slices.Sort(parked) var begun []*actionFrame for _, id := range parked { - perf, err := e.beginPerformance(parent, e.graphOf(parent), node, nil) + perf, err := e.beginPerformance(parent, e.graphOf(parent), node, nil, nil) if err != nil { return nil, err } @@ -336,9 +336,7 @@ func (e *ActionExecutor) endAround(tokenIdx int, perf *actionFrame) error { e.dropTokensIn(perf, id) if perf == e.root { e.removeToken(e.tokenIndex(id)) - e.state = StateCompleted - e.ctx.endPerformanceLife(e.occurrence) - return nil + return e.completeRoot() } return e.leaveTerminated(e.tokenIndex(id), perf) } diff --git a/internal/exec/runtime/analysis_run.go b/internal/exec/runtime/analysis_run.go index 6d1e4c6bdb..735a2729ce 100644 --- a/internal/exec/runtime/analysis_run.go +++ b/internal/exec/runtime/analysis_run.go @@ -100,21 +100,21 @@ func (shape *calcShape) subjectParameter() (*calcParameter, bool) { // enclosingSubject is the subject of the case whose body declares shape's usage, // which a nested case binding no subject of its own takes (SysML v2 §7.21.2): // read from the environment of the evaluation reading the usage. -func (ctx *Context) enclosingSubject(shape *calcShape, enclosing *EvalContext) (Value, bool) { +func (ctx *Context) enclosingSubject(shape *calcShape, enclosing *EvalContext) (Value, bool, error) { if enclosing == nil { - return Value{}, false + return Value{}, false, nil } owner := enclosingBehavior(shape.Sym) if owner == nil || !isCalcSymbol(owner) { - return Value{}, false + return Value{}, false, nil } outer, err := ctx.calcShapeOf(owner) if err != nil { - return Value{}, false + return Value{}, false, err } subject, ok := outer.subjectParameter() if !ok { - return Value{}, false + return Value{}, false, nil } return enclosing.Lookup(subject.Name) } @@ -698,7 +698,16 @@ func (ctx *Context) analysisVerdict(kind, name string, check conditionCheck, con // bindingsFrame is the run's bindings as a frame the case owns, so a condition reads // its features by qualified name (`MassCase::result`) and its steps' pins (`step.out`). func (run *calcRun) bindingsFrame(ctx *Context) frame { - f := frame{vars: run.bindings(ctx), perf: run.perf, owner: run.shape, run: run.env.run} + vars := run.bindings(ctx) + f := frame{vars: vars, perf: run.perf, owner: run.shape, run: run.env.run} + for _, body := range run.bodyFrames { + for name := range body.unvalued { + name = canonical(body.aliases, name) + if _, bound := vars[name]; !bound { + f.markUnvalued(name) + } + } + } f.write = calcFeatureWriter(ctx, run.shape, run.occurrence) return f } @@ -708,6 +717,14 @@ func (run *calcRun) bindingsFrame(ctx *Context) frame { func (run *calcRun) bindings(ctx *Context) map[string]Value { bindings := make(map[string]Value, run.env.width()+len(run.shape.Outputs)) run.env.each(func(name string, value Value) { bindings[name] = value }) + for _, body := range run.bodyFrames { + for name := range body.unvalued { + delete(bindings, canonical(body.aliases, name)) + } + for name, value := range body.vars { + bindings[canonical(body.aliases, name)] = value + } + } for _, out := range run.shape.Outputs { if out.Name == "" { continue diff --git a/internal/exec/runtime/assign_chain.go b/internal/exec/runtime/assign_chain.go index 5d56371996..dec2a21d56 100644 --- a/internal/exec/runtime/assign_chain.go +++ b/internal/exec/runtime/assign_chain.go @@ -68,11 +68,15 @@ func (ec *EvalContext) chainCarrier(target *lower.AssignTarget) (*Instance, erro // names: a part names an occurrence rather than a value, and a calc usage names // an evaluation, which holds no object a write could reach. func (ec *EvalContext) chainRoot(base ast.Node) (Value, error) { - if sym, ok := ec.calcUsageOperand(base); ok { + if sym, ok, err := ec.calcUsageOperand(base); err != nil { + return Value{}, err + } else if ok { return Value{}, fmt.Errorf("%w: calc usage %s computes output features and holds no object", ErrTypeMismatch, symbolText(sym)) } - if sym, ok := ec.occurrenceOperand(base); ok { + if sym, ok, err := ec.occurrenceOperand(base); err != nil { + return Value{}, err + } else if ok { // A collection reads as its objects; the step after it writes on one of them. if ec.ctx.namesObjects(sym) { return ec.ctx.denotedValue(sym) diff --git a/internal/exec/runtime/body_bindings.go b/internal/exec/runtime/body_bindings.go new file mode 100644 index 0000000000..e24e409971 --- /dev/null +++ b/internal/exec/runtime/body_bindings.go @@ -0,0 +1,558 @@ +package runtime + +import ( + "fmt" + "maps" + + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// bodyBindingCheckError identifies a value rejected by its body's declaration. +type bodyBindingCheckError struct { + err error +} + +// Error returns the underlying declaration-check error. +func (e bodyBindingCheckError) Error() string { return e.err.Error() } + +// Unwrap exposes the declaration-check error to errors.Is and errors.As. +func (e bodyBindingCheckError) Unwrap() error { return e.err } + +// bodyCells holds one runtime store and its dependency-graph cells. +type bodyCells struct { + vars map[string]Value + cells map[string]*bodyCell + order []string + context func(*symbols.Scope) *EvalContext +} + +// bodyCell pairs a stored value with its dependency-graph cell. +type bodyCell struct { + owner *bodyCells + name string + fv FeatureValue + binding *bodyBinding +} + +// bodyBinding records the expression and declaration context a cell tracks. +type bodyBinding struct { + value ast.Node + scope *symbols.Scope + masked string + check func(*Value) error + context func(*symbols.Scope) *EvalContext + onDerived func(*Value) error + visible []map[string]bool + limitFrames bool + frozen bool +} + +// occurrenceHasRedefinedDefault reports whether an occurrence holds an overriding declaration's default. +func occurrenceHasRedefinedDefault(fv *FeatureValue, attr lower.Attribute, fallback *symbols.Scope) bool { + if fv == nil || fv.Feature == nil || fv.Feature.DefaultDecl == nil { + return false + } + scope := attr.Scope + if scope == nil { + scope = fallback + } + declared := memberSymbol(scope, attr.Node) + return declared != nil && fv.Feature.DefaultDecl != declared +} + +// bodyCellState is the observable tracking state of one body cell. +type bodyCellState struct { + cell *bodyCell + written bool + frozen bool + materialized bool + tracking bool +} + +// bodyCellsCapture preserves a store's maps and binding metadata for restoration. +type bodyCellsCapture struct { + cells *bodyCells + vars mapState[string, Value] + members map[string]*bodyCell + bindings map[*bodyCell]*bodyBinding + order []string +} + +// captureBodyCells snapshots the maps and binding metadata without deriving values. +func captureBodyCells(cells *bodyCells) bodyCellsCapture { + capture := bodyCellsCapture{cells: cells} + if cells == nil { + return capture + } + capture.vars = captureMap(cells.vars) + capture.members = maps.Clone(cells.cells) + capture.order = append([]string(nil), cells.order...) + capture.bindings = make(map[*bodyCell]*bodyBinding, len(cells.cells)) + for _, cell := range cells.cells { + if cell.binding == nil { + continue + } + binding := *cell.binding + binding.visible = cloneBodyBindingVisibility(binding.visible) + capture.bindings[cell] = &binding + } + return capture +} + +// restore reinstates the maps and bindings held by the capture. +func (c bodyCellsCapture) restore() { + if c.cells == nil { + return + } + c.cells.vars = c.vars.restore() + if c.cells.cells == nil { + c.cells.cells = make(map[string]*bodyCell, len(c.members)) + } + clear(c.cells.cells) + c.cells.order = append(c.cells.order[:0], c.order...) + for name, cell := range c.members { + cell.owner, cell.name = c.cells, name + if saved := c.bindings[cell]; saved != nil { + binding := *saved + binding.visible = cloneBodyBindingVisibility(saved.visible) + cell.binding = &binding + } else { + cell.binding = nil + } + c.cells.cells[name] = cell + } +} + +// newBodyCells wraps a value store with a lazy dependency-cell map. +func newBodyCells(vars map[string]Value, context func(*symbols.Scope) *EvalContext) *bodyCells { + return &bodyCells{vars: vars, cells: make(map[string]*bodyCell), context: context} +} + +// cell returns the named cell, creating a source cell for an existing value. +func (cells *bodyCells) cell(name string) *bodyCell { + if cells == nil { + return nil + } + if cell := cells.cells[name]; cell != nil { + return cell + } + cell := &bodyCell{owner: cells, name: name} + cell.fv.body = cell + if value, ok := cells.vars[name]; ok { + cell.fv.Value = value + cell.fv.Materialized = true + } + cells.cells[name] = cell + cells.order = append(cells.order, name) + return cell +} + +// existingCell returns the named cell without creating one. +func (cells *bodyCells) existingCell(name string) *bodyCell { + if cells == nil { + return nil + } + return cells.cells[name] +} + +// registerBodyBinding adds an expression-backed cell using the store's context. +func (ctx *Context) registerBodyBinding( + cells *bodyCells, + name string, + value ast.Node, + scope *symbols.Scope, + check func(*Value) error, + onDerived func(*Value) error, +) *bodyCell { + return ctx.registerBodyBindingInContext(cells, name, value, scope, check, nil, onDerived) +} + +// registerBodyBindingInContext adds an expression-backed cell with a custom context. +func (ctx *Context) registerBodyBindingInContext( + cells *bodyCells, + name string, + value ast.Node, + scope *symbols.Scope, + check func(*Value) error, + context func(*symbols.Scope) *EvalContext, + onDerived func(*Value) error, +) *bodyCell { + cell := cells.cell(name) + cell.binding = &bodyBinding{ + value: value, scope: scope, check: check, context: context, onDerived: onDerived, + } + return cell +} + +// deriveBodyCell evaluates an unmaterialized binding and records its dependencies. +func (ctx *Context) deriveBodyCell(cells *bodyCells, name string, cell *bodyCell) (Value, error) { + if cell == nil || cell.binding == nil || cell.binding.frozen || cell.fv.Written { + value, ok := cells.vars[name] + if !ok { + return Value{}, &NoValueError{Feature: name} + } + return value, nil + } + if cell.fv.Materialized { + return cells.vars[name], nil + } + if ctx.derivingValue(&cell.fv) { + return Value{}, fmt.Errorf("%w: %s", ErrCyclicFeatureValue, name) + } + value, err := ctx.deriveWith(&cell.fv, func() (Value, error) { + if cells.context == nil { + return Value{}, fmt.Errorf("body binding %s has no evaluation context", name) + } + ec := cells.context(cell.binding.scope) + if cell.binding.context != nil { + ec = cell.binding.context(cell.binding.scope) + } + if ec == nil { + return Value{}, fmt.Errorf("body binding %s has no evaluation context", name) + } + if cell.binding.limitFrames { + frames := append([]frame(nil), ec.frames...) + if len(frames) > len(cell.binding.visible) { + frames = frames[:len(cell.binding.visible)] + } + for i := range frames { + if i < len(cell.binding.visible) { + frames[i].visible = cell.binding.visible[i] + } else { + frames[i].visible = map[string]bool{} + } + } + contextCopy := *ec + contextCopy.frames = frames + ec = &contextCopy + } + value, err := ec.Eval(cell.binding.value) + if err != nil { + return Value{}, err + } + if cell.binding.check != nil { + if err := cell.binding.check(&value); err != nil { + return Value{}, bodyBindingCheckError{err: err} + } + } + if cell.binding.onDerived != nil { + if err := cell.binding.onDerived(&value); err != nil { + return Value{}, err + } + } + ctx.noteProbeWrite(&cell.fv) + cell.fv.Value, cell.fv.Values, cell.fv.Materialized = value, Value{}, true + cell.fv.intrinsic = false + cells.vars[name] = value + return value, nil + }) + return value, err +} + +// seedBodyBindingFromFeatureValue reuses a matching occurrence value and its read edges. +func (ctx *Context) seedBodyBindingFromFeatureValue(cells *bodyCells, name string, source *FeatureValue) (Value, bool, error) { + cell := cells.existingCell(name) + if cell == nil || cell.binding == nil || source == nil || !source.Materialized || source.Written || source.body != nil { + return Value{}, false, nil + } + value := source.HeldValue() + if value.Kind == ValInvalid { + return Value{}, false, nil + } + if cell.binding.check != nil { + if err := cell.binding.check(&value); err != nil { + return Value{}, false, bodyBindingCheckError{err: err} + } + } + reads := append([]*FeatureValue(nil), source.reads...) + ctx.forgetReads(&cell.fv) + ctx.noteProbeWrite(&cell.fv) + for _, read := range reads { + ctx.listRead(read, &cell.fv) + } + cell.fv.Value, cell.fv.Values, cell.fv.Materialized = value, Value{}, true + cell.fv.Written, cell.fv.BindingDerived, cell.fv.intrinsic = false, false, false + cells.vars[name] = value + if cell.binding.onDerived != nil { + if err := cell.binding.onDerived(&value); err != nil { + return Value{}, false, err + } + cell.fv.Value = value + cells.vars[name] = value + } + ctx.noteRead(nil, &cell.fv) + return value, true, nil +} + +// cloneBodyBindingVisibility copies the lexical visibility captured by a binding. +func cloneBodyBindingVisibility(visible []map[string]bool) []map[string]bool { + if visible == nil { + return nil + } + cloned := make([]map[string]bool, len(visible)) + for i, frame := range visible { + cloned[i] = maps.Clone(frame) + } + return cloned +} + +// readBodyCell returns the current value and records it as a dependency when needed. +func (ctx *Context) readBodyCell(cells *bodyCells, name string) (Value, bool, error) { + if cells == nil { + return Value{}, false, nil + } + cell := cells.cells[name] + if cell == nil && len(ctx.deriving) != 0 { + if _, ok := cells.vars[name]; ok { + cell = cells.cell(name) + } + } + if cell == nil { + value, ok := cells.vars[name] + return value, ok, nil + } + if cell.binding != nil && !cell.binding.frozen && !cell.fv.Written && !cell.fv.Materialized { + value, err := ctx.deriveBodyCell(cells, name, cell) + if err != nil { + return Value{}, false, err + } + ctx.noteRead(nil, &cell.fv) + return value, true, nil + } + value, ok := cells.vars[name] + if ok { + ctx.noteRead(nil, &cell.fv) + } + return value, ok, nil +} + +// writeBodyCell stores a value and ends tracking when the cell is a binding. +func (ctx *Context) writeBodyCell(cells *bodyCells, name string, value Value) { + if cells == nil { + return + } + cell := cells.cells[name] + if cell == nil { + cells.vars[name] = value + return + } + held := ctx.beforeWrite(&cell.fv) + if !held.taken { + ctx.noteProbeWrite(&cell.fv) + } + cells.vars[name] = value + cell.fv.Value, cell.fv.Values, cell.fv.Materialized = value, Value{}, true + cell.fv.intrinsic = false + if cell.binding != nil { + cell.fv.Written = true + ctx.forgetReads(&cell.fv) + } + ctx.afterWrite(&cell.fv, held) +} + +// writeBodyValue stores a semantic value through its dependency cell when present. +func (ctx *Context) writeBodyValue(cells *bodyCells, vars map[string]Value, name string, value Value) { + if cells == nil { + vars[name] = value + return + } + ctx.writeBodyCell(cells, name, value) +} + +// mirrorBodyCell exposes a body's tracked value through the matching occurrence feature. +func (ctx *Context) mirrorBodyCell(inst *Instance, name string, cell *bodyCell, value Value) (Value, error) { + if inst == nil { + return value, nil + } + fv, ok := inst.FeatureValues[name] + if !ok { + return value, nil + } + ctx.noteProbeWrite(fv) + fv.body = cell + deriving := ctx.derivingValue(fv) + if !deriving { + if err := inst.BindFeatureValue(ctx, name, value); err != nil { + return value, err + } + } + fv.Written = false + ctx.listRead(&cell.fv, fv) + if deriving { + return value, nil + } + return fv.HeldValue(), nil +} + +// clearBodyValue clears a stored value and invalidates its dependents. +func (ctx *Context) clearBodyValue(cells *bodyCells, vars map[string]Value, name string) { + if cells == nil || cells.cells[name] == nil { + delete(vars, name) + return + } + cell := cells.cells[name] + held := ctx.beforeWrite(&cell.fv) + if !held.taken { + ctx.noteProbeWrite(&cell.fv) + } + delete(vars, name) + cell.fv.Value, cell.fv.Values, cell.fv.Materialized = Value{}, Value{}, false + cell.fv.intrinsic = false + if cell.binding != nil { + cell.fv.Written = true + ctx.forgetReads(&cell.fv) + } + ctx.afterWrite(&cell.fv, held) +} + +// bodyCellStateOf reports the stored and tracking state of a cell. +func bodyCellStateOf(cells *bodyCells, name string) bodyCellState { + if cells == nil || cells.cells[name] == nil { + return bodyCellState{} + } + cell := cells.cells[name] + state := bodyCellState{cell: cell, materialized: cell.fv.Materialized, written: cell.fv.Written} + if cell.binding != nil { + state.frozen = cell.binding.frozen + state.tracking = !state.written && !state.frozen + } + return state +} + +// restoreBodyCellState reinstates a captured cell state and its dependency edges. +func (ctx *Context) restoreBodyCellState(cells *bodyCells, name string, state bodyCellState) { + if state.cell == nil { + return + } + cell := state.cell + cell.fv.Written = state.written + if cell.binding != nil { + cell.binding.frozen = state.frozen + } + if state.tracking { + ctx.forgetReads(&cell.fv) + delete(cells.vars, name) + cell.fv.Value, cell.fv.Values, cell.fv.Materialized = Value{}, Value{}, false + cell.fv.intrinsic = false + ctx.invalidateDependents(&cell.fv) + return + } + cell.fv.Materialized = state.materialized + if state.materialized { + cell.fv.Value = cells.vars[name] + } else { + cell.fv.Value = Value{} + } +} + +// readBodyValue reads a value from its dependency-aware store. +func (ctx *Context) readBodyValue(cells *bodyCells, vars map[string]Value, name string) (Value, bool, error) { + if cells == nil { + value, ok := vars[name] + return value, ok, nil + } + return ctx.readBodyCell(cells, name) +} + +// deriveBodyCells settles the still-tracking cells of a store. +func (ctx *Context) deriveBodyCells(cells *bodyCells) error { + if cells == nil { + return nil + } + for _, name := range cells.order { + cell := cells.cells[name] + if cell.binding == nil || cell.binding.frozen || cell.fv.Written || cell.fv.Materialized { + continue + } + if _, err := ctx.deriveBodyCell(cells, name, cell); err != nil { + return err + } + } + return nil +} + +// freezeBodyCells derives and freezes every still-tracking cell in a store. +func (ctx *Context) freezeBodyCells(cells *bodyCells) error { + if err := ctx.deriveBodyCells(cells); err != nil { + return err + } + if cells == nil { + return nil + } + for _, name := range cells.order { + cell := cells.cells[name] + if cell.binding == nil || cell.binding.frozen { + continue + } + ctx.noteProbeWrite(&cell.fv) + cell.binding.frozen = true + ctx.forgetReads(&cell.fv) + } + return nil +} + +// restartBodyCells makes frozen, unwritten bindings eligible to derive again. +func (ctx *Context) restartBodyCells(cells *bodyCells) { + if cells == nil { + return + } + for _, name := range cells.order { + cell := cells.cells[name] + if cell.binding == nil || !cell.binding.frozen || cell.fv.Written { + continue + } + ctx.invalidateDependents(&cell.fv) + ctx.noteProbeWrite(&cell.fv) + ctx.forgetReads(&cell.fv) + delete(cells.vars, name) + cell.fv.Value, cell.fv.Values, cell.fv.Materialized = Value{}, Value{}, false + cell.fv.BindingDerived, cell.fv.intrinsic = false, false + cell.binding.frozen = false + } +} + +// forgetBodyCells removes the dependencies of every binding in a store. +func (ctx *Context) forgetBodyCells(cells *bodyCells) { + if cells == nil { + return + } + for _, name := range cells.order { + cell := cells.cells[name] + if cell.binding != nil { + ctx.forgetReads(&cell.fv) + cell.binding.frozen = true + } + } +} + +// exitBodyCells invalidates values owned by an exiting state. +func (ctx *Context) exitBodyCells(cells *bodyCells) { + if cells == nil { + return + } + for _, name := range cells.order { + cell := cells.cells[name] + if cell.binding == nil { + continue + } + ctx.invalidateDependents(&cell.fv) + ctx.noteProbeWrite(&cell.fv) + cell.binding.frozen = true + ctx.forgetReads(&cell.fv) + } +} + +// resetBodyCells removes the stored cells and their dependency edges. +func (ctx *Context) resetBodyCells(cells *bodyCells) { + if cells == nil { + return + } + ctx.forgetBodyCells(cells) + cells.cells = make(map[string]*bodyCell) + cells.order = nil + for name := range cells.vars { + delete(cells.vars, name) + } +} diff --git a/internal/exec/runtime/calc_statements.go b/internal/exec/runtime/calc_statements.go index d9f99af055..e354f8888c 100644 --- a/internal/exec/runtime/calc_statements.go +++ b/internal/exec/runtime/calc_statements.go @@ -71,7 +71,7 @@ func (h *calcStmtHost) attachPerformances(engine *stmtEngine) { subactions: make(map[ast.Node]*actionFrame), nodes: h.shape.Nodes, label: h.shape.Label, - outer: append(append([]frame{}, engine.env.enclosing...), engine.env.data), + outer: append(append(append([]frame{}, engine.env.enclosing...), engine.env.data), engine.env.localFrame()), run: h.ctx.newRun(), } h.flow = &ActionExecutor{ @@ -130,6 +130,13 @@ func (h *calcStmtHost) declaredOutput(name string) bool { // assignOuter binds an output this calculation declares, and rejects any other // undeclared name: writing that would be an effect outside the calculation. func (h *calcStmtHost) assignOuter(env *stmtEnv, name string, value Value, s lower.Assign) error { + if env.rootDeclares(name) { + if err := h.ctx.checkBodyWrite(h, s, &value); err != nil { + return err + } + env.assignRootLocal(h.ctx, name, value) + return h.mirrorOccurrence(name, value) + } if !h.declaredOutput(name) { return fmt.Errorf("%w: %s is not declared by the calculation", ErrCalcExternalAssignment, name) } @@ -187,6 +194,24 @@ func (h *calcStmtHost) mirrorOccurrence(name string, value Value) error { return nil } +// mirrorRootLocal mirrors a calc's root local into its performance occurrence. +func (h *calcStmtHost) mirrorRootLocal(name string, value Value) error { + return h.mirrorOccurrence(name, value) +} + +// mirrorBodyBinding keeps an occurrence feature linked to a calc body binding. +func (h *calcStmtHost) mirrorBodyBinding(name string, cell *bodyCell, value *Value) error { + if h.occ == nil || h.occ.inst == nil { + return nil + } + mirrored, err := h.ctx.mirrorBodyCell(h.occ.inst, name, cell, *value) + if err != nil { + return err + } + *value = mirrored + return nil +} + // assignChain rejects a chained target: writing a feature of another object is // an effect outside the calculation, as writing an undeclared name is. func (h *calcStmtHost) assignChain(_ *EvalContext, s lower.Assign, _ Value) error { @@ -271,7 +296,11 @@ func (h *calcStmtHost) effect(_ *stmtEngine, s lower.Effect) error { if !ok { return fmt.Errorf("%s: 'perform' names no action to perform", h.describe()) } - _, outputs, err := invokeAction(h.ctx, s.Scope, inv, h.env.values(), h.self) + values, err := h.env.values(h.ctx) + if err != nil { + return fmt.Errorf("%s: %w", h.describe(), err) + } + _, outputs, err := invokeAction(h.ctx, s.Scope, inv, values, h.self) if err != nil { return fmt.Errorf("%s: %w", h.describe(), err) } @@ -281,8 +310,8 @@ func (h *calcStmtHost) effect(_ *stmtEngine, s lower.Effect) error { } sort.Strings(names) for _, name := range names { - if !h.env.assign(name, outputs[name]) { - h.env.data.set(name, outputs[name]) + if !h.env.assign(h.ctx, name, outputs[name]) { + h.env.data.setBody(h.ctx, name, outputs[name]) } } return nil @@ -314,8 +343,8 @@ func (h *calcStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, } nestedOrder = graph.StatementOrders[node] } - engine.env.enter() - defer engine.env.leave() + engine.env.enter(engine) + defer engine.env.leave(engine.ctx, true) defer engine.enterActivation()() for _, feature := range graph.Features[node] { if feature.Value == nil { @@ -326,7 +355,7 @@ func (h *calcStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, if err != nil { return flowNext, fmt.Errorf("eval %s of %s: %w", feature.Name, nodeDescription(node), err) } - engine.env.declare(feature.Name, value) + engine.env.declare(engine.ctx, feature.Name, value) } if nestedSteps != nil { return engine.runWithOrder(nestedSteps, nestedOrder) @@ -359,14 +388,14 @@ func (h *calcStmtHost) setFeature(name string, value Value) error { // assignAround writes what a step's performance returns to the same-named value // of the body: an output the case declares, or a parameter or local it holds. func (h *calcStmtHost) assignAround(name string, value Value) (bool, error) { - if h.env.assignLocal(name, value) { + if h.env.assignLocal(h.ctx, name, value) { return true, nil } if h.declaredOutput(name) || h.env.data.has(name) { if err := h.ctx.checkNamedWrite(h.shape.bodyScope(), h.describe(), name, &value); err != nil { return true, err } - h.env.data.set(name, value) + h.env.data.setBody(h.ctx, name, value) return true, h.mirrorOccurrence(name, value) } return false, nil diff --git a/internal/exec/runtime/calc_usage.go b/internal/exec/runtime/calc_usage.go index 4c17cf61a7..9ff98aa327 100644 --- a/internal/exec/runtime/calc_usage.go +++ b/internal/exec/runtime/calc_usage.go @@ -427,6 +427,9 @@ type calcRun struct { // activation is the execution this run is, so a usage its outputs read is // evaluated once for the whole run rather than once per output. activation int64 + // bodyFrames are the root statement locals left by the run, which output + // bindings read after its statements have completed. + bodyFrames []frame // perf is the case's performance, whose steps an output binding reads by name // (`step.pin`); nil for a calc, which performs none. perf *actionFrame @@ -438,6 +441,16 @@ type calcRun struct { occurrence *calcOccurrence } +// lookup searches the calc's body frames before its enclosing evaluation frame. +func (run *calcRun) lookup(name string) (Value, bool) { + for i := len(run.bodyFrames) - 1; i >= 0; i-- { + if value, ok := run.bodyFrames[i].lookup(name); ok { + return value, true + } + } + return run.env.lookup(name) +} + // boundInputs are the values each non-subject input parameter of shape was // bound to in env, in declaration order. func boundInputs(shape *calcShape, env frame) []InputBinding { @@ -479,6 +492,7 @@ func (run *calcRun) detached() *calcRun { } out := *run out.env = run.env.snapshot() + out.bodyFrames = snapshotFrames(run.bodyFrames) if run.outer != nil { out.outer = run.outer.closure() } @@ -979,6 +993,8 @@ func (ctx *Context) runCalcUsage(start *calcUsageStart) (*calcRun, error) { steps, _ = shape.observationSteps() } result, returned, err := runCalcSteps(engine, host, steps) + run.env.cells = engine.env.data.cells + run.bodyFrames = []frame{engine.env.localFrame().snapshot()} if err != nil { if paused(err) { return nil, err @@ -1027,6 +1043,11 @@ func (run *calcRun) endOccurrence(ctx *Context) { // its binding the first time it is read. func (run *calcRun) output(ctx *Context, name string) (Value, error) { if value, ok := run.outputs[name]; ok { + if out, found := run.shape.output(name); found && (out.Value == nil || out.IsInitial) { + if _, held := run.env.lookup(out.Name); held { + return run.readBodyOutput(ctx, out.Name) + } + } return value, nil } out, ok := run.shape.output(name) @@ -1046,6 +1067,21 @@ func (run *calcRun) output(ctx *Context, name string) (Value, error) { return value, nil } +// readBodyOutput returns a calc output after deriving its body-local dependencies. +func (run *calcRun) readBodyOutput(ctx *Context, name string) (Value, error) { + if run.env.cells == nil && len(ctx.deriving) != 0 { + run.env.cells = newBodyCells(run.env.vars, nil) + } + value, ok, err := run.env.read(ctx, name) + if err != nil { + return Value{}, err + } + if !ok { + return Value{}, &NoValueError{Feature: name} + } + return value, nil +} + // value evaluates one output feature's binding in the scope of the calc that // declares it, with the calc's parameters and locals in scope, and memoizes it // for this evaluation. An output that names another output has it evaluated on @@ -1063,7 +1099,11 @@ func (run *calcRun) value(ctx *Context, out calcOutput) (Value, error) { if out.Value == nil || out.IsInitial { // An output the body assigned, and an `inout` the invocation bound, are values // the activation left behind rather than bindings to evaluate. - if value, ok := run.env.lookup(out.Name); ok && out.Name != "" { + if _, ok := run.env.lookup(out.Name); ok && out.Name != "" { + value, err := run.readBodyOutput(ctx, out.Name) + if err != nil { + return Value{}, err + } run.outputs[out.Name] = value return value, nil } @@ -1138,6 +1178,9 @@ func (run *calcRun) bindingEnv(ctx *Context, owner *symbols.Symbol) *EvalContext if run.perf != nil { ec.pushFrame(performanceFrame(run.perf)) } + for _, frame := range run.bodyFrames { + ec.pushFrame(frame) + } ec.calcRun = run return ec } @@ -1246,60 +1289,72 @@ func (run *calcRun) nestedUsage(ctx *Context, name string) (*calcRun, bool, erro // usage, whose members are computed rather than declared values. A local binding // or valued feature of the same name is the value the expression names, so it // masks the declaration. -func (ec *EvalContext) calcUsageOperand(operand ast.Node) (*symbols.Symbol, bool) { +func (ec *EvalContext) calcUsageOperand(operand ast.Node) (*symbols.Symbol, bool, error) { ref, ok := operand.(*ast.FeatureReference) if !ok || ref.Name == nil || len(ref.Name.Parts) == 0 || ec.ctx.model.resolver == nil { - return nil, false + return nil, false, nil } - if len(ref.Name.Parts) == 1 && ec.namesValue(ref.Name.Parts[0].Text) { - return nil, false + if len(ref.Name.Parts) == 1 { + names, err := ec.namesValue(ref.Name.Parts[0].Text) + if err != nil { + return nil, false, err + } + if names { + return nil, false, nil + } } sym, ok := ec.ctx.resolveQualified(ec.scope, ref.Name) if !ok || !isCalcUsageSymbol(sym) { - return nil, false + return nil, false, nil } - return sym, true + return sym, true, nil } // namesValue reports whether name is a local binding or a valued feature of the // element being evaluated, which the expression names rather than a declaration. -func (ec *EvalContext) namesValue(name string) bool { - if _, bound := ec.Lookup(name); bound { - return true +func (ec *EvalContext) namesValue(name string) (bool, error) { + if _, bound, err := ec.Lookup(name); err != nil { + return false, err + } else if bound { + return true, nil } _, valued := ec.valuedFeature(name) - return valued + return valued, nil } // occurrenceOperand reports whether the operand of a feature chain names occurrences of // its own — a part or item, or a namespace's collection of them — that no local binding // or valued feature, and no feature value of the object being evaluated, already answers with. -func (ec *EvalContext) occurrenceOperand(operand ast.Node) (*symbols.Symbol, bool) { +func (ec *EvalContext) occurrenceOperand(operand ast.Node) (*symbols.Symbol, bool, error) { ref, ok := operand.(*ast.FeatureReference) if !ok || ref.Name == nil || len(ref.Name.Parts) == 0 || ec.ctx.model.resolver == nil { - return nil, false + return nil, false, nil } if len(ref.Name.Parts) == 1 { name := ref.Name.Parts[0].Text - if ec.namesValue(name) { - return nil, false + names, err := ec.namesValue(name) + if err != nil { + return nil, false, err + } + if names { + return nil, false, nil } if ec.self != nil { if _, carried := ec.self.FeatureValues[name]; carried { - return nil, false + return nil, false, nil } } } sym, ok := ec.ctx.resolveQualified(ec.scope, ref.Name) if !ok || !ec.ctx.namesOneObject(sym) && !ec.ctx.namesObjects(sym) { - return nil, false + return nil, false, nil } if isReferenceUsage(sym) { // A reference member of a behavior — `Raise::context` — holds the value // its run bound, not an occurrence of its own: read it as a value. - return nil, false + return nil, false, nil } - return sym, true + return sym, true, nil } // calcUsageMemberValue reads parts from a calc usage a part declares, running it diff --git a/internal/exec/runtime/case_step.go b/internal/exec/runtime/case_step.go index f2c8241c95..43654220f1 100644 --- a/internal/exec/runtime/case_step.go +++ b/internal/exec/runtime/case_step.go @@ -99,7 +99,7 @@ func (e *performances) performCase(perf *actionFrame) error { } for _, out := range outputs { perf.features[out.Name] = ast.DirOut - perf.data[perf.key(out.Name)] = out.Value + e.ctx.writeBodyValue(perf.cells, perf.data, perf.key(out.Name), out.Value) } if out := run.shape.resultOutput(); out != nil { perf.result = out.Name diff --git a/internal/exec/runtime/check_body.go b/internal/exec/runtime/check_body.go index 7a9ee83295..a6f234c301 100644 --- a/internal/exec/runtime/check_body.go +++ b/internal/exec/runtime/check_body.go @@ -89,7 +89,7 @@ func (h *stateStmtHost) spell(s *stateSpeller) string { // spell of an engine is the frames its statements read past its data — the // blocks it paused in keep theirs, so these are the values declared outside any. func (f *engineFrame) spell(s *stateSpeller) string { - return "engine{" + s.localFrames(f.engine.env.frames, f.engine.env.unvalued) + "}" + return "engine{" + s.localBodyFrames(f.engine.env) + "}" } func (f *stmtListFrame) spell(s *stateSpeller) string { @@ -133,13 +133,45 @@ func (f *branchFrame) spell(*stateSpeller) string { } func (f *blockFrame) spell(s *stateSpeller) string { - return "block{" + s.locals(f.locals, f.unvalued) + "}" + return "block{" + s.bodyLocals(f.locals, f.cells, f.unvalued) + "}" } func (f *flowNodeFrame) spell(*stateSpeller) string { return "flow at " + nodeKey(f.node) } func (f *loopFrame) spell(s *stateSpeller) string { - return fmt.Sprintf("loop %d of (%s){%s}", f.iteration, s.elements(f.elements), s.locals(f.locals, f.unvalued)) + return fmt.Sprintf("loop %d of (%s){%s}", f.iteration, s.elements(f.elements), s.bodyLocals(f.locals, f.cells, f.unvalued)) +} + +func (s *stateSpeller) bodyLocals(locals map[string]Value, cells *bodyCells, unvalued map[string]bool) string { + spelled := s.bodyValues(locals, cells) + names := make([]string, 0, len(unvalued)) + for name := range unvalued { + names = append(names, name) + } + sort.Strings(names) + if len(names) > 0 { + spelled += " unvalued " + strings.Join(names, ", ") + } + return spelled +} + +func (s *stateSpeller) localBodyFrames(env *stmtEnv) string { + locals, cells := env.localFrames() + frames := make([]string, len(locals)) + for i, values := range locals { + var unvalued map[string]bool + if i == 0 { + unvalued = env.unvaluedLocal + } else if i-1 < len(env.unvalued) { + unvalued = env.unvalued[i-1] + } + var localCells *bodyCells + if i < len(cells) { + localCells = cells[i] + } + frames[i] = "{" + s.bodyLocals(values, localCells, unvalued) + "}" + } + return strings.Join(frames, " ") } func (f *performFrame) spell(s *stateSpeller) string { @@ -177,30 +209,3 @@ func (f *subflowFrame) spell(s *stateSpeller) string { func (f *calleeFrame) spell(s *stateSpeller) string { return "callee " + f.name + " " + s.nested(f.exec) } - -// locals spells the values a block or loop declared, the unvalued marked. -func (s *stateSpeller) locals(locals map[string]Value, unvalued map[string]bool) string { - names := make([]string, 0, len(unvalued)) - for name := range unvalued { - names = append(names, name) - } - sort.Strings(names) - spelled := s.values(locals) - if len(names) > 0 { - spelled += " unvalued " + strings.Join(names, ", ") - } - return spelled -} - -// localFrames spells a stack of local frames, outermost first. -func (s *stateSpeller) localFrames(frames []map[string]Value, unvalued []map[string]bool) string { - parts := make([]string, len(frames)) - for i, locals := range frames { - var marks map[string]bool - if i < len(unvalued) { - marks = unvalued[i] - } - parts[i] = "{" + s.locals(locals, marks) + "}" - } - return strings.Join(parts, " ") -} diff --git a/internal/exec/runtime/check_state.go b/internal/exec/runtime/check_state.go index bbbd46f2eb..1a2d4c6342 100644 --- a/internal/exec/runtime/check_state.go +++ b/internal/exec/runtime/check_state.go @@ -346,6 +346,32 @@ func (s *stateSpeller) joinsArrived(e *StateExecutor) { } s.out.WriteString("}") } + fmt.Fprintf(&s.out, " data{%s}", s.bodyValues(e.stateData, e.stateCells)) + for _, state := range sortedStates(e.stateAttrs) { + fmt.Fprintf(&s.out, " attrs{%s: %s}", e.statePath(state), s.bodyValues(e.stateAttrs[state], e.stateAttrCells[state])) + } + fmt.Fprintf(&s.out, " visits{%s}", strings.Join(e.stateVisits, ", ")) + if e.eventQueue != nil { + events := slices.Clone(e.eventQueue.events) + sort.SliceStable(events, func(i, j int) bool { return events.Less(i, j) }) + for _, event := range events { + fmt.Fprintf(&s.out, " event{%s}", s.event(e, event)) + } + } + for _, trans := range sortedTransitions(e.timerScheduled) { + fmt.Fprintf(&s.out, " timer{%s}", s.transition(e, trans)) + } + for _, trans := range sortedTransitions(e.changeFired) { + fmt.Fprintf(&s.out, " latched{%s}", s.transition(e, trans)) + } + for _, act := range e.doActions { + fmt.Fprintf(&s.out, " do{%s: %d pending", e.statePath(act.state), len(act.pending)) + if act.run != nil { + fmt.Fprintf(&s.out, ", paused{%s}", s.body(act.run.body)) + } + s.out.WriteString("}") + } + s.out.WriteByte('\n') } // stateName spells a state by its path in the machine, nothing for none. @@ -521,9 +547,13 @@ func (s *stateSpeller) frame(perf *actionFrame) string { fmt.Fprintf(&b, " %s", group) } fmt.Fprintf(&b, " live=%d", perf.live) - fmt.Fprintf(&b, " data{%s}", s.values(perf.data)) - for _, local := range perf.locals { - fmt.Fprintf(&b, " local{%s}", s.values(local)) + fmt.Fprintf(&b, " data{%s}", s.bodyValues(perf.data, perf.cells)) + for i, local := range perf.locals { + var cells *bodyCells + if i < len(perf.localCells) { + cells = perf.localCells[i] + } + fmt.Fprintf(&b, " local{%s}", s.bodyValues(local, cells)) } s.deliveries(&b, perf) for _, node := range sortedNodes(perf.subactions) { @@ -727,6 +757,52 @@ func (s *stateSpeller) values(m map[string]Value) string { return strings.Join(parts, ", ") } +// bodyValues spells stored values and the tracking state of body bindings. +func (s *stateSpeller) bodyValues(values map[string]Value, cells *bodyCells) string { + if cells == nil { + return s.values(values) + } + names := make(map[string]bool, len(values)+len(cells.cells)) + for name := range values { + names[name] = true + } + for name, cell := range cells.cells { + if cell.binding != nil { + names[name] = true + } + } + ordered := make([]string, 0, len(names)) + for name := range names { + ordered = append(ordered, name) + } + sort.Strings(ordered) + parts := make([]string, 0, len(ordered)) + for _, name := range ordered { + cell := cells.cells[name] + value, held := values[name] + if cell != nil && cell.binding != nil { + state := "tracking" + switch { + case cell.fv.Written: + state = "written" + case cell.binding.frozen: + state = "frozen" + } + if !cell.fv.Materialized { + held = false + } + text := UnsetText + if held { + text = s.value(value) + } + parts = append(parts, fmt.Sprintf("%s = %s [%s]", name, text, state)) + } else if held { + parts = append(parts, name+" = "+s.value(value)) + } + } + return strings.Join(parts, ", ") +} + func (s *stateSpeller) elements(elements []Value) string { parts := make([]string, len(elements)) for i, element := range elements { diff --git a/internal/exec/runtime/classifier_behavior.go b/internal/exec/runtime/classifier_behavior.go index 304177cd4a..9da4a06d42 100644 --- a/internal/exec/runtime/classifier_behavior.go +++ b/internal/exec/runtime/classifier_behavior.go @@ -1113,7 +1113,7 @@ func (ctx *Context) attachClassifierBehavior(inst *Instance, decl classifierBeha return nil, fmt.Errorf("exhibited state machine %s of %s: %w", decl.behavior.Name, symbolText(inst.Type), err) } for name, value := range arguments { - exec.stateData[name] = value + exec.ctx.writeBodyValue(exec.stateCells, exec.stateData, name, value) } if err := exec.initialize(); err != nil { exec.Release() diff --git a/internal/exec/runtime/conformance_test.go b/internal/exec/runtime/conformance_test.go index 3e6f1d0a26..38d1dbaa09 100644 --- a/internal/exec/runtime/conformance_test.go +++ b/internal/exec/runtime/conformance_test.go @@ -770,9 +770,18 @@ func admissibleSchemaProblems(expected ExpectedOutcome, oracleTitles map[string] if expected.Admissible != "" { problems = append(problems, "admissible is stated without outcomes to admit") } - if expected.ExploreBudget != nil { + hasSingleOutcome := expected.Outputs != nil || expected.FinalState != "" || expected.StateVisits != nil || expected.Terminated + if expected.ExploreBudget != nil && !hasSingleOutcome { problems = append(problems, "exploreBudget is stated without outcomes to explore") } + if expected.ExploreBudget != nil && hasSingleOutcome { + if expected.Type != "action" && expected.Type != "state" { + problems = append(problems, fmt.Sprintf("exploreBudget applies to action and state cases, not %q", expected.Type)) + } + if _, err := ExplorePolicy(expected.ExploreBudget.budget()); err != nil { + problems = append(problems, "exploreBudget: "+err.Error()) + } + } return problems } if _, err := ExplorePolicy(expected.ExploreBudget.budget()); err != nil { @@ -2510,12 +2519,16 @@ func TestAdmissibleOutcomesSchema(t *testing.T) { {Outputs: map[string]ExpectedValue{"x": one}}, {Outputs: map[string]ExpectedValue{"x": two}}, } + runs := 65536 tests := []struct { name string expected ExpectedOutcome problems int }{ {"single outcome", ExpectedOutcome{Type: "action", Outputs: outcomes[0].Outputs}, 0}, + {"single outcome explore budget", ExpectedOutcome{ + Type: "action", Outputs: outcomes[0].Outputs, ExploreBudget: &ExpectedExploreBudget{Runs: &runs}, + }, 0}, {"admissible set", ExpectedOutcome{Type: "action", Outcomes: outcomes, Admissible: cited}, 0}, {"state admissible set", ExpectedOutcome{Type: "state", Outcomes: []AdmittedOutcome{{FinalState: "A"}, {FinalState: "B"}}, Admissible: cited}, 0}, {"outcomes beside outputs", ExpectedOutcome{Type: "action", Outputs: outcomes[0].Outputs, Outcomes: outcomes, Admissible: cited}, 1}, diff --git a/internal/exec/runtime/constraint_statements.go b/internal/exec/runtime/constraint_statements.go index bcc441f4b4..92b9beb4e9 100644 --- a/internal/exec/runtime/constraint_statements.go +++ b/internal/exec/runtime/constraint_statements.go @@ -117,8 +117,8 @@ func (h *constraintStmtHost) performNode(engine *stmtEngine, graph *lower.Action } nestedOrder = graph.StatementOrders[node] } - engine.env.enter() - defer engine.env.leave() + engine.env.enter(engine) + defer engine.env.leave(engine.ctx, true) defer engine.enterActivation()() for _, feature := range graph.Features[node] { if feature.Value == nil { @@ -129,7 +129,7 @@ func (h *constraintStmtHost) performNode(engine *stmtEngine, graph *lower.Action if err != nil { return flowNext, fmt.Errorf("eval %s of %s: %w", feature.Name, nodeDescription(node), err) } - engine.env.declare(feature.Name, value) + engine.env.declare(engine.ctx, feature.Name, value) } if nestedSteps != nil { return engine.runWithOrder(nestedSteps, nestedOrder) @@ -187,15 +187,16 @@ func (ctx *Context) runConstraintSteps(steps *BodySteps, features map[string]sco enclosing = append(enclosing, bindings) } host := &constraintStmtHost{ctx: ctx, self: self, scope: steps.Scope, steps: steps} + var engine *stmtEngine _, err := ctx.runStatements(func() *stmtEngine { - engine := newStmtEngineIn(ctx, host, data, enclosing) + engine = newStmtEngineIn(ctx, host, data, enclosing) engine.features = features return engine }, steps.Stmts) if err != nil { return frame{}, err } - return data, nil + return engine.env.constraintResult(ctx) } // statementOrder returns the lowered order when scheduling or precedence needs it. diff --git a/internal/exec/runtime/context.go b/internal/exec/runtime/context.go index c1a8305597..909bb16ad6 100644 --- a/internal/exec/runtime/context.go +++ b/internal/exec/runtime/context.go @@ -1628,7 +1628,7 @@ func (ctx *Context) ExecuteActionPerformedBy(action *symbols.Symbol, self *Insta return nil, err } // Return the values the action's features hold once it completed - return exec.Results(), nil + return exec.ResultsWithError() } // ActionOutcomePerformedBy runs an action as ExecuteActionPerformedBy does and reports @@ -1648,7 +1648,11 @@ func (ctx *Context) ExecuteActionReportingPerformer(action *symbols.Symbol, self if err != nil { return nil, nil, err } - return exec.Results(), (&Invocation{Actions: []*ActionExecutor{exec}}).PerformerAttributes(), nil + outputs, err = exec.ResultsWithError() + if err != nil { + return nil, nil, err + } + return outputs, (&Invocation{Actions: []*ActionExecutor{exec}}).PerformerAttributes(), nil } // performAction runs action to completion, performed by self, and returns the @@ -1829,7 +1833,11 @@ func (ctx *Context) ExecuteStatePerformedBy(stateMachine *symbols.Symbol, self * return nil, nil, err } // Return state machine data and the real ordered visit trace - return exec.StateData(), exec.GetStateVisits(), nil + data, err := exec.StateDataWithError() + if err != nil { + return nil, nil, err + } + return data, exec.GetStateVisits(), nil } // StateOutcomeWithEvents runs a state machine as ExecuteStateWithEvents does and @@ -1845,6 +1853,9 @@ func (ctx *Context) StateOutcomePerformedBy(stateMachine *symbols.Symbol, self * if err != nil { return Outcome{}, err } + if _, err := exec.StateDataWithError(); err != nil { + return Outcome{}, err + } return (&Invocation{States: []*StateExecutor{exec}}).Outcome(), nil } diff --git a/internal/exec/runtime/dependents.go b/internal/exec/runtime/dependents.go index 6261a96fc0..fe051420bd 100644 --- a/internal/exec/runtime/dependents.go +++ b/internal/exec/runtime/dependents.go @@ -6,7 +6,7 @@ package runtime // derivable reports whether fv's value is derived from what it reads: bound by `=`, // neither a fallback default nor assigned by a run nor propagated by a binding. func (ctx *Context) derivable(fv *FeatureValue) bool { - return !fv.Written && !fv.BindingDerived && !fv.Feature.DefaultIsFallback() + return !fv.Written && !fv.BindingDerived && (fv.Feature == nil || !fv.Feature.DefaultIsFallback()) } // derivation is a `=` value being derived; stale once a value it read was written @@ -26,15 +26,15 @@ func (ctx *Context) deriveFeatureValue(inst *Instance, fv *FeatureValue, name st clean, reads := ctx.endTrace(top) return val, clean, reads, err } - for { - ctx.forgetReads(fv) + var clean bool + var reads []sharedRead + val, err := ctx.deriveWith(fv, func() (Value, error) { top := ctx.beginTrace(inst, fv) - val, stale, err := ctx.deriveOnce(inst, fv, name) - clean, reads := ctx.endTrace(top) - if err != nil || !stale { - return val, clean, reads, err - } - } + value, err := ctx.evalFeatureValueDefault(inst, fv, name) + clean, reads = ctx.endTrace(top) + return value, err + }) + return val, clean, reads, err } // forgetReads delists fv from what its last derivation read: this one reads afresh, @@ -98,18 +98,39 @@ func (ctx *Context) forgetEdgesOf(objects []*Instance) { } } -// deriveOnce derives fv under a frame of its own, reporting whether it went stale. -func (ctx *Context) deriveOnce(inst *Instance, fv *FeatureValue, name string) (val Value, stale bool, err error) { +// deriveWith evaluates fv under a frame of its own, retrying when it went stale. +func (ctx *Context) deriveWith(fv *FeatureValue, eval func() (Value, error)) (Value, error) { + for { + ctx.forgetReads(fv) + val, stale, err := ctx.deriveOnceWith(fv, eval) + if err != nil || !stale { + return val, err + } + } +} + +// deriveOnceWith evaluates one dependency-tracked attempt and reports invalidation. +func (ctx *Context) deriveOnceWith(fv *FeatureValue, eval func() (Value, error)) (val Value, stale bool, err error) { top := len(ctx.deriving) ctx.deriving = append(ctx.deriving, derivation{fv: fv}) defer func() { stale = ctx.deriving[top].stale ctx.deriving = ctx.deriving[:top] }() - val, err = ctx.evalFeatureValueDefault(inst, fv, name) + val, err = eval() return val, false, err } +// derivingValue reports whether fv is already on the active derivation stack. +func (ctx *Context) derivingValue(fv *FeatureValue) bool { + for i := range ctx.deriving { + if ctx.deriving[i].fv == fv { + return true + } + } + return false +} + // noteRead lists the value being derived, if any, as a dependent of the fv just read, // held by inst, and fv among what it reads; a derivation being observed sees the read. func (ctx *Context) noteRead(inst *Instance, fv *FeatureValue) { @@ -310,6 +331,9 @@ func (ctx *Context) invalidate(dependents []*FeatureValue) (deriving []*FeatureV default: ctx.noteProbeWrite(dep) dep.Value, dep.Values, dep.Materialized, dep.intrinsic = Value{}, Value{}, false, false + if dep.body != nil && dep.body.owner != nil { + delete(dep.body.owner.vars, dep.body.name) + } ctx.invalidateDependents(dep) } } diff --git a/internal/exec/runtime/eval.go b/internal/exec/runtime/eval.go index 126b71980a..cbda382774 100644 --- a/internal/exec/runtime/eval.go +++ b/internal/exec/runtime/eval.go @@ -317,13 +317,15 @@ func (ec *EvalContext) Pop() { } // Lookup searches for a name in the frame stack (innermost first). -func (ec *EvalContext) Lookup(name string) (Value, bool) { +func (ec *EvalContext) Lookup(name string) (Value, bool, error) { for i := len(ec.frames) - 1; i >= 0; i-- { - if val, ok := ec.frames[i].lookup(name); ok { - return val, true + if val, ok, err := ec.frames[i].read(ec.ctx, name); err != nil { + return Value{}, false, err + } else if ok { + return val, true, nil } } - return Value{}, false + return Value{}, false, nil } // Eval evaluates an expression node. Returns a Value or an error. @@ -630,7 +632,9 @@ func (ec *EvalContext) evalName(qn *ast.QualifiedName) (Value, error) { // Outside an expression body no body-local declaration can shadow a bound // name, so a frame binding is the answer: the common case, kept small. if qn != nil && len(qn.Parts) == 1 && (ec.scope == nil || !ec.scope.BodyLocal()) { - if val, ok := ec.Lookup(qn.Parts[0].Text); ok { + if val, ok, err := ec.Lookup(qn.Parts[0].Text); err != nil { + return Value{}, err + } else if ok { return val, nil } } @@ -672,7 +676,9 @@ func (ec *EvalContext) evalNameGeneral(qn *ast.QualifiedName) (Value, error) { } } // Try frame stack first (local bindings from calc/lambda params) - if val, ok := ec.Lookup(name); ok { + if val, ok, err := ec.Lookup(name); err != nil { + return Value{}, err + } else if ok { return val, nil } // Then a node of an action performance in the frame stack, read as a value. @@ -846,7 +852,9 @@ func (ec *EvalContext) evalNameGeneral(qn *ast.QualifiedName) (Value, error) { // A feature of a behavior whose run is on the stack (`MassCase::result` in its // objective or assertion) reads the value that run bound to it. if qualifier, ok := reading.Part(len(qn.Parts) - 2); ok { - if val, ok := ec.frameFeatureValue(qualifier, currentSym); ok { + if val, ok, err := ec.frameFeatureValue(qualifier, currentSym); err != nil { + return Value{}, err + } else if ok { return val, nil } } @@ -922,9 +930,12 @@ func (ec *EvalContext) evalNameGeneral(qn *ast.QualifiedName) (Value, error) { // frameFeatureValue reads the resolved member sym, qualified by qualifier, from the innermost // frame whose owner is (or specializes) the qualifier, under the name that owner's run binds it by. -func (ec *EvalContext) frameFeatureValue(qualifier, sym *symbols.Symbol) (Value, bool) { +func (ec *EvalContext) frameFeatureValue(qualifier, sym *symbols.Symbol) (Value, bool, error) { for i := len(ec.frames) - 1; i >= 0; i-- { f := ec.frames[i] + if f.lexical { + continue + } if f.owner != nil { if !f.owner.qualifiedBy(ec.ctx, qualifier) { continue @@ -933,8 +944,10 @@ func (ec *EvalContext) frameFeatureValue(qualifier, sym *symbols.Symbol) (Value, if !ok { continue } - if val, ok := f.lookup(name); ok { - return val, true + if val, ok, err := f.read(ec.ctx, name); err != nil { + return Value{}, false, err + } else if ok { + return val, true, nil } continue } @@ -944,11 +957,13 @@ func (ec *EvalContext) frameFeatureValue(qualifier, sym *symbols.Symbol) (Value, if !f.runs(ec.ctx, qualifier) { continue } - if val, ok := f.lookup(sym.Name); ok { - return val, true + if val, ok, err := f.read(ec.ctx, sym.Name); err != nil { + return Value{}, false, err + } else if ok { + return val, true, nil } } - return Value{}, false + return Value{}, false, nil } // writeFrameFeature is frameFeatureValue for a write: value goes into the innermost @@ -959,6 +974,9 @@ func (ec *EvalContext) frameFeatureValue(qualifier, sym *symbols.Symbol) (Value, func (ec *EvalContext) writeFrameFeature(qualifier, sym *symbols.Symbol, value Value) (bool, error) { for i := len(ec.frames) - 1; i >= 0; i-- { f := ec.frames[i] + if f.lexical { + continue + } if f.owner != nil { if !f.owner.qualifiedBy(ec.ctx, qualifier) { continue @@ -1374,13 +1392,17 @@ func (ec *EvalContext) evalFeatureChain(n *ast.FeatureChainExpr) (Value, error) // A calc usage carries no value of its own: its output features are computed // by evaluating it, so `c.a` runs the usage — once — and reads the output // from that evaluation rather than from a feature value. - if sym, ok := ec.calcUsageOperand(base); ok { + if sym, ok, err := ec.calcUsageOperand(base); err != nil { + return Value{}, err + } else if ok { return ec.evalCalcUsageMembers(sym, parts) } // A part carries no value of its own: it denotes an occurrence, whose features // `lander.mass.mDry` reads, so the chain is read from that object. - if sym, ok := ec.occurrenceOperand(base); ok { + if sym, ok, err := ec.occurrenceOperand(base); err != nil { + return Value{}, err + } else if ok { // A usage of an enclosing object is read from that object, so a sibling // chain `e1.length` inside `e3` reads the containing rectangle's e1. if val, ok, err := ec.outerFeatureValue(sym); ok { diff --git a/internal/exec/runtime/frame.go b/internal/exec/runtime/frame.go index 011e4e3071..2be53170a6 100644 --- a/internal/exec/runtime/frame.go +++ b/internal/exec/runtime/frame.go @@ -1,6 +1,8 @@ package runtime import ( + "maps" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" ) @@ -8,8 +10,12 @@ import ( // frame is one level of local bindings an evaluation reads: a calc invocation's // parameter slots, a map of named values, or both. type frame struct { - slots *slotFrame - vars map[string]Value + slots *slotFrame + vars map[string]Value + cells *bodyCells + ensureCells func() *bodyCells + masked map[string]bool + visible map[string]bool // unvalued marks names the frame declares but holds no value for: a read // answers as missing, a write binds. unvalued map[string]bool @@ -22,6 +28,8 @@ type frame struct { // owner is the calc whose parameters, locals and outputs the frame binds, so a // qualified name of one of its members (`MassCase::result`) reads the binding. owner *calcShape + // lexical marks local statement bindings, which are not qualified members. + lexical bool // write is the run's own write path for a feature the frame binds — checked // against its declaration and mirrored or streamed the way the run's // statements write it — nil where the frame is a snapshot or plain bindings, @@ -124,12 +132,22 @@ func (f frame) performs() *symbols.Symbol { // withVars is the frame holding vars in place of its own, still answering for // the same run and performance. func (f frame) withVars(vars map[string]Value) frame { - return frame{vars: vars, aliases: f.aliases, perf: f.perf, owner: f.owner, write: f.write, performed: f.performed, run: f.run, merged: f.merged, firing: f.firing, machine: f.machine} + return frame{ + vars: vars, masked: f.masked, visible: f.visible, unvalued: f.unvalued, + aliases: f.aliases, perf: f.perf, owner: f.owner, lexical: f.lexical, write: f.write, + performed: f.performed, run: f.run, merged: f.merged, firing: f.firing, machine: f.machine, + } } // lookup finds name in the frame: a slot binding it, else the map. func (f frame) lookup(name string) (Value, bool) { name = canonical(f.aliases, name) + if f.visible != nil && !f.visible[name] { + return Value{}, false + } + if f.unvalued[name] { + return Value{}, false + } if f.slots != nil { if value, ok := f.slots.lookup(name); ok { return value, true @@ -139,6 +157,30 @@ func (f frame) lookup(name string) (Value, bool) { return value, ok } +// read resolves a name and derives its body cell when the stored value is stale. +func (f frame) read(ctx *Context, name string) (Value, bool, error) { + name = canonical(f.aliases, name) + if f.masked[name] || (f.visible != nil && !f.visible[name]) { + return Value{}, false, nil + } + if f.unvalued[name] { + return Value{}, false, &NoValueError{Feature: name} + } + if f.slots != nil { + if value, ok := f.slots.lookup(name); ok { + return value, true, nil + } + } + if f.cells == nil && len(ctx.deriving) != 0 && f.ensureCells != nil { + f.cells = f.ensureCells() + } + if f.cells == nil { + value, ok := f.vars[name] + return value, ok, nil + } + return ctx.readBodyCell(f.cells, name) +} + // has reports whether the frame binds name or declares it unvalued. func (f frame) has(name string) bool { if f.unvalued[name] { @@ -150,7 +192,7 @@ func (f frame) has(name string) bool { // 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) { +func (f *frame) markUnvalued(name string) { if f.unvalued == nil { f.unvalued = map[string]bool{} } @@ -160,15 +202,31 @@ func (f frame) markUnvalued(name string) { // 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) + delete(f.unvalued, name) if f.slots != nil && f.slots.set(name, value) { return } + f.vars[name] = value +} + +// setBody writes through the frame's dependency cell when one is present. +func (f frame) setBody(ctx *Context, name string, value Value) { + name = canonical(f.aliases, name) delete(f.unvalued, name) + if f.slots != nil && f.slots.set(name, value) { + return + } + if f.cells != nil { + ctx.writeBodyCell(f.cells, name, value) + return + } f.vars[name] = value } // bindParam binds parameter i, named name: by position in the slots, else by name. func (f frame) bindParam(i int, name string, value Value) { + name = canonical(f.aliases, name) + delete(f.unvalued, name) if f.slots != nil { f.slots.bind(i, value) return @@ -179,13 +237,50 @@ func (f frame) bindParam(i int, name string, value Value) { // each calls fn for every name the frame binds. func (f frame) each(fn func(name string, value Value)) { if f.slots != nil { - f.slots.each(fn) + f.slots.each(func(name string, value Value) { + name = canonical(f.aliases, name) + if !f.unvalued[name] && (f.visible == nil || f.visible[name]) { + fn(name, value) + } + }) } for name, value := range f.vars { - fn(name, value) + name = canonical(f.aliases, name) + if !f.unvalued[name] && (f.visible == nil || f.visible[name]) { + fn(name, value) + } } } +// visibleNames returns the visible names bound by the frame and its cells. +func (f frame) visibleNames() map[string]bool { + names := make(map[string]bool, len(f.vars)) + f.each(func(name string, _ Value) { names[canonical(f.aliases, name)] = true }) + if f.cells != nil { + for _, name := range f.cells.order { + names[canonical(f.aliases, name)] = true + } + } + for name := range f.unvalued { + name = canonical(f.aliases, name) + if f.visible == nil || f.visible[name] { + names[name] = true + } + } + return names +} + +// eachCurrent visits frame values after settling its tracked bindings. +func (f frame) eachCurrent(ctx *Context, fn func(name string, value Value)) error { + if f.cells != nil { + if err := ctx.deriveBodyCells(f.cells); err != nil { + return err + } + } + f.each(fn) + return nil +} + // snapshot copies the frame's bindings, and the aliases they are read through, // into storage of its own, unchanged by whatever later reuses the frame's. The // copy still answers for the run it was taken from, though not for its flow's nodes. @@ -193,7 +288,14 @@ func (f frame) snapshot() frame { vars := make(map[string]Value, f.width()) f.each(func(name string, value Value) { vars[name] = value }) out := ownedFrame(f.owner, vars) - out.performed, out.run, out.merged = f.performs(), f.run, f.merged + out.performed, out.run, out.merged, out.lexical = f.performs(), f.run, f.merged, f.lexical + if f.visible != nil { + out.visible = make(map[string]bool, len(f.visible)) + for name, visible := range f.visible { + out.visible[name] = visible + } + } + out.unvalued = maps.Clone(f.unvalued) if len(f.aliases) > 0 { out.aliases = make(map[string]string, len(f.aliases)) for name, alias := range f.aliases { diff --git a/internal/exec/runtime/function_value.go b/internal/exec/runtime/function_value.go index 6bc1aee6c1..ef649d24aa 100644 --- a/internal/exec/runtime/function_value.go +++ b/internal/exec/runtime/function_value.go @@ -160,7 +160,9 @@ func (ec *EvalContext) boundFunction(callee *symbols.Symbol, qn *ast.QualifiedNa return Value{}, false, nil } name := qn.Parts[0].Text - if val, ok := ec.Lookup(name); ok { + if val, ok, err := ec.Lookup(name); err != nil { + return Value{}, false, err + } else if ok { return val, true, nil } // A calc-typed feature the element being evaluated binds (`in calc :>> f = g`) @@ -208,7 +210,9 @@ func (ec *EvalContext) declaredFunction(callee *symbols.Symbol) (Value, bool, er // else the calc the callee's own value names. func (ec *EvalContext) qualifiedBoundFunction(callee *symbols.Symbol, qn *ast.QualifiedName) (Value, bool, error) { if qualifier, ok := ec.ctx.readQualified(ec.scope, qn).Part(len(qn.Parts) - 2); ok { - if val, ok := ec.frameFeatureValue(qualifier, callee); ok { + if val, ok, err := ec.frameFeatureValue(qualifier, callee); err != nil { + return Value{}, true, err + } else if ok { return val, true, nil } } diff --git a/internal/exec/runtime/held_image_behavior.go b/internal/exec/runtime/held_image_behavior.go index 4cf083c371..6377ff78bc 100644 --- a/internal/exec/runtime/held_image_behavior.go +++ b/internal/exec/runtime/held_image_behavior.go @@ -61,7 +61,9 @@ type imagedFrame struct { saved actionFrame parent int locals []map[string]Value + localCells [][]imagedBodyCell data map[string]Value + cells []imagedBodyCell outer []imagedOuter subactions map[ast.Node]int repeats map[repetitionGroupID]imagedRepetition @@ -71,6 +73,18 @@ type imagedFrame struct { nested map[ast.Node][]nestedDelivery } +// imagedBodyCell stores a body binding's value and tracking state for an image. +type imagedBodyCell struct { + name string + value ast.Node + scope *symbols.Scope + masked string + written bool + frozen bool + visible []map[string]bool + limitFrames bool +} + type imagedRepetition struct { node ast.Node remaining int64 @@ -103,7 +117,9 @@ type imagedState struct { nextEventID int64 events []Event stateData map[string]Value + stateCells []imagedBodyCell stateAttrs map[*ast.StateNode]map[string]Value + stateAttrCells map[*ast.StateNode][]imagedBodyCell stateVisits []string stateStack []*ast.StateNode fired []FiredTransition @@ -233,6 +249,7 @@ func (t *imaging) frame(perf *actionFrame, at func(*actionFrame) int) (imagedFra } f := imagedFrame{saved: *perf, parent: at(perf.parent)} f.saved.parent, f.saved.locals, f.saved.outer, f.saved.data = nil, nil, nil, nil + f.saved.cells, f.saved.localCells = nil, nil f.saved.subactions, f.saved.repeats, f.saved.pending, f.saved.held, f.saved.staged, f.saved.nested = nil, nil, nil, nil, nil, nil f.saved.connections = slices.Clone(perf.connections) f.saved.features = maps.Clone(perf.features) @@ -241,16 +258,23 @@ func (t *imaging) frame(perf *actionFrame, at func(*actionFrame) int) (imagedFra f.saved.nodes = slices.Clone(perf.nodes) f.saved.streamed = maps.Clone(perf.streamed) f.saved.unreceived = cloneUnreceived(perf.unreceived) - for _, local := range perf.locals { - if err := t.values(local); err != nil { + for i, local := range perf.locals { + cloned := maps.Clone(local) + var cells []imagedBodyCell + if i < len(perf.localCells) { + cells = imageBodyCells(perf.localCells[i], cloned) + } + if err := t.values(cloned); err != nil { return imagedFrame{}, err } - f.locals = append(f.locals, maps.Clone(local)) + f.locals = append(f.locals, cloned) + f.localCells = append(f.localCells, cells) } - if err := t.values(perf.data); err != nil { + f.data = maps.Clone(perf.data) + f.cells = imageBodyCells(perf.cells, f.data) + if err := t.values(f.data); err != nil { return imagedFrame{}, err } - f.data = maps.Clone(perf.data) outer, err := t.outerFrames(perf, at) if err != nil { return imagedFrame{}, err @@ -375,6 +399,7 @@ func (t *imaging) stateExecutor(e *StateExecutor) (*imagedState, error) { graph: e.graph, state: e.state, activeConfig: cloneConfiguration(e.activeConfig), nextEventID: e.nextEventID, stateData: maps.Clone(e.stateData), + stateAttrCells: make(map[*ast.StateNode][]imagedBodyCell, len(e.stateAttrCells)), stateAttrs: make(map[*ast.StateNode]map[string]Value, len(e.stateAttrs)), stateVisits: slices.Clone(e.stateVisits), stateStack: slices.Clone(e.stateStack), @@ -409,7 +434,8 @@ func (t *imaging) stateExecutor(e *StateExecutor) (*imagedState, error) { if img.run, err = t.run(e.driven.state); err != nil { return nil, err } - if err := t.values(e.stateData); err != nil { + img.stateCells = imageBodyCells(e.stateCells, img.stateData) + if err := t.values(img.stateData); err != nil { return nil, err } if call := e.pendingCall; call != nil { @@ -421,10 +447,12 @@ func (t *imaging) stateExecutor(e *StateExecutor) (*imagedState, error) { } } for node, attrs := range e.stateAttrs { - if err := t.values(attrs); err != nil { + cloned := maps.Clone(attrs) + img.stateAttrCells[node] = imageBodyCells(e.stateAttrCells[node], cloned) + if err := t.values(cloned); err != nil { return nil, fmt.Errorf("state %s: %w", StateVertexName(node), err) } - img.stateAttrs[node] = maps.Clone(attrs) + img.stateAttrs[node] = cloned } for node, record := range e.history { img.history[node] = historyRecord{child: record.child, regions: maps.Clone(record.regions)} @@ -629,7 +657,9 @@ func (m *materializing) actionExecutor(e *ActionExecutor, img *imagedAction) err // frame fills one performance of dst's from its image. func (m *materializing) frame(perf *actionFrame, img imagedFrame, frameAt func(int) *actionFrame) error { + perfs := perf.perfs *perf = img.saved + perf.perfs = perfs perf.parent = frameAt(img.parent) perf.connections = slices.Clone(img.saved.connections) perf.features = maps.Clone(img.saved.features) @@ -640,16 +670,21 @@ func (m *materializing) frame(perf *actionFrame, img imagedFrame, frameAt func(i perf.unreceived = cloneUnreceived(img.saved.unreceived) var err error perf.locals = nil - for _, local := range img.locals { + perf.localCells = make([]*bodyCells, len(img.locals)) + for i, local := range img.locals { carried, err := m.values(local) if err != nil { return err } perf.locals = append(perf.locals, carried) + if i < len(img.localCells) { + perf.localCells[i] = m.frameCells(perf, carried, img.localCells[i], i+1) + } } if perf.data, err = m.values(img.data); err != nil { return err } + perf.cells = m.frameCells(perf, perf.data, img.cells, 0) perf.outer = nil for _, outer := range img.outer { vars, err := m.values(outer.vars) @@ -687,9 +722,155 @@ func (m *materializing) frame(perf *actionFrame, img imagedFrame, frameAt func(i if err := m.nested(perf, img.nested); err != nil { return err } + for _, cells := range perf.localCells { + if err := m.dst.deriveBodyCells(cells); err != nil { + return err + } + } + if err := m.dst.deriveBodyCells(perf.cells); err != nil { + return err + } return nil } +// imageBodyCells captures binding declarations and flags without dependency edges. +func imageBodyCells(cells *bodyCells, values map[string]Value) []imagedBodyCell { + if cells == nil { + return nil + } + // Tracking cells omit dependency edges from images and derive again on first read. + var image []imagedBodyCell + for _, name := range cells.order { + cell := cells.cells[name] + if cell.binding == nil { + continue + } + image = append(image, imagedBodyCell{ + name: name, value: cell.binding.value, scope: cell.binding.scope, + masked: cell.binding.masked, + written: cell.fv.Written, frozen: cell.binding.frozen, + visible: cloneBodyBindingVisibility(cell.binding.visible), limitFrames: cell.binding.limitFrames, + }) + if !cell.fv.Written && !cell.binding.frozen { + delete(values, name) + } + } + return image +} + +// restoreStateBodyCells reinstates the binding cells owned by a state image. +func restoreStateBodyCells( + ctx *Context, + cells *bodyCells, + attributes []lower.Attribute, + image []imagedBodyCell, + owner string, + defaultScope *symbols.Scope, +) { + features := make(map[string]lower.Attribute, len(attributes)) + for _, attr := range attributes { + features[attr.Name] = attr + } + for _, state := range image { + attr, ok := features[state.name] + if !ok || !attr.Binding || attr.Value == nil { + continue + } + if cell := cells.cells[state.name]; cell != nil && cell.binding != nil { + restoreBodyCellValue(cells, state, cell) + continue + } + scope := attr.Scope + if scope == nil { + scope = defaultScope + } + check := func(value *Value) error { + return ctx.checkBodyDeclaration(scope, owner, state.name, value) + } + cell := ctx.registerBodyBinding(cells, state.name, attr.Value, scope, check, nil) + cell.binding.visible = cloneBodyBindingVisibility(state.visible) + cell.binding.limitFrames = state.limitFrames + restoreBodyCellValue(cells, state, cell) + } +} + +// restoreBodyCellValue restores a body's value and written or tracking state. +func restoreBodyCellValue(cells *bodyCells, state imagedBodyCell, cell *bodyCell) { + if value, held := cells.vars[state.name]; held { + cell.fv.Value, cell.fv.Materialized = value, true + } + if state.written { + cell.fv.Written = true + } else if state.frozen { + cell.binding.frozen = true + } else { + delete(cells.vars, state.name) + cell.fv.Value, cell.fv.Values, cell.fv.Materialized = Value{}, Value{}, false + } +} + +// frameCells rebuilds the dependency cells for a materialized performance frame. +func (m *materializing) frameCells( + perf *actionFrame, + values map[string]Value, + image []imagedBodyCell, + localDepth int, +) *bodyCells { + if len(image) == 0 { + return nil + } + defaultContext := func(scope *symbols.Scope) *EvalContext { + ec := perf.perfs.evalContextFor(perf, scope) + if localDepth > 0 { + localStart := len(ec.frames) - len(perf.locals) - 1 + ec.frames = ec.frames[:localStart+localDepth] + } + return ec + } + cells := newBodyCells(values, defaultContext) + for _, state := range image { + if state.value == nil { + continue + } + check := func(value *Value) error { + return m.dst.checkBodyDeclaration(state.scope, perf.describe(), state.name, value) + } + var onDerived func(*Value) error + if perf.node == nil && perf.parent == nil { + if owner, ok := perf.perfs.owner.(*ActionExecutor); ok { + cell := cells.cell(state.name) + onDerived = func(value *Value) error { + mirrored, err := owner.mirrorBindingOccurrence(state.name, *value, cell) + if err != nil { + return err + } + *value = mirrored + return nil + } + } + } + context := func(scope *symbols.Scope) *EvalContext { + if state.masked != "" { + return perf.perfs.evalBindingContext(perf, scope, state.masked, perf.began) + } + return defaultContext(scope) + } + cell := m.dst.registerBodyBindingInContext(cells, state.name, state.value, state.scope, check, context, onDerived) + cell.binding.visible = cloneBodyBindingVisibility(state.visible) + cell.binding.masked = state.masked + cell.binding.limitFrames = state.limitFrames + if state.written { + cell.fv.Written = true + } else if state.frozen { + cell.binding.frozen = true + } else { + delete(values, state.name) + cell.fv.Value, cell.fv.Materialized = Value{}, false + } + } + return cells +} + // pending fills perf's pending table from img's. func (m *materializing) pending(perf *actionFrame, pending map[ast.Node]map[string][]Value) error { if pending == nil { @@ -780,10 +961,23 @@ func (m *materializing) stateExecutor(e *StateExecutor, img *imagedState) error if e.stateData == nil { e.stateData = make(map[string]Value) } + if e.stateCells != nil { + e.stateCells.vars = e.stateData + restoreStateBodyCells(m.dst, e.stateCells, e.graph.Attributes, img.stateCells, + "state machine "+symbolText(e.stateMachine), e.graph.Scope) + } for node, attrs := range img.stateAttrs { if e.stateAttrs[node], err = m.values(attrs); err != nil { return fmt.Errorf("state %s: %w", StateVertexName(node), err) } + if image := img.stateAttrCells[node]; len(image) != 0 { + cells := newBodyCells(e.stateAttrs[node], func(scope *symbols.Scope) *EvalContext { + return e.stateAttributeContext(node, scope) + }) + restoreStateBodyCells(m.dst, cells, e.graph.StateAttributes[node], image, + "state "+node.Name, e.graph.Scope) + e.stateAttrCells[node] = cells + } } e.stateVisits, e.stateStack = slices.Clone(img.stateVisits), slices.Clone(img.stateStack) e.fired, e.firedBase, e.keepFired = slices.Clone(img.fired), img.firedBase, img.keepFired diff --git a/internal/exec/runtime/instance.go b/internal/exec/runtime/instance.go index 1607964af7..5bb5408bd3 100644 --- a/internal/exec/runtime/instance.go +++ b/internal/exec/runtime/instance.go @@ -81,6 +81,7 @@ func (inst *Instance) keepConnector(fv *FeatureValue, id int64) { // Feature value holds the runtime value(s) for one feature. type FeatureValue struct { Feature *EffectiveFeature + body *bodyCell Value Value // scalar feature value (multiplicity [1]) Values Value // collection feature value (Sequence or Set) Materialized bool // lazy flag: has this feature value been instantiated? @@ -1535,6 +1536,16 @@ func (ctx *Context) evalFeatureValueDefault(inst *Instance, fv *FeatureValue, na } ctx.derivingFeatureValues[key] = true defer delete(ctx.derivingFeatureValues, key) + if fv.body != nil { + value, ok, err := ctx.readBodyCell(fv.body.owner, fv.body.name) + if err != nil { + return Value{}, err + } + if !ok { + return Value{}, &NoValueError{Feature: name} + } + return value, nil + } scope := fv.Feature.DefaultScope() if scope == nil { diff --git a/internal/exec/runtime/invoke_calc.go b/internal/exec/runtime/invoke_calc.go index aee7a63327..13eef48285 100644 --- a/internal/exec/runtime/invoke_calc.go +++ b/internal/exec/runtime/invoke_calc.go @@ -645,6 +645,7 @@ type invocationFrame struct { // slots hold the parameters; bindings the locals the body declares beside them. slots slotFrame bindings map[string]Value + cells *bodyCells aliases map[string]string owner *calcShape // the calc invoked, whose members the locals bind run int64 // the run this invocation is (Context.newRun) @@ -655,7 +656,7 @@ type invocationFrame struct { // locals is the frame the invocation's parameters and body locals are bound in. func (f *invocationFrame) locals() frame { - return frame{slots: &f.slots, vars: f.bindings, aliases: f.aliases, owner: f.owner, run: f.run} + return frame{slots: &f.slots, vars: f.bindings, cells: f.cells, aliases: f.aliases, owner: f.owner, run: f.run} } // maxFreeInvocationFrames bounds the frames kept, so one deep recursion does not @@ -684,6 +685,7 @@ func (ctx *Context) acquireInvocationFrame() *invocationFrame { // caller has ended the activation, so nothing memoized still reads the bindings. func (ctx *Context) releaseInvocationFrame(frame *invocationFrame) { bindings, slots := frame.bindings, frame.slots + ctx.forgetBodyCells(frame.env.data.cells) if frame.locals().width() > maxPooledBindings { bindings, slots = nil, slotFrame{} } else { @@ -940,7 +942,9 @@ func (ctx *Context) bindCalcParameters( // activation, which the caller ends after it. func (ctx *Context) runCalcBody(shape *calcShape, frame *invocationFrame, callerScope *symbols.Scope, self *Instance, activation int64, enclosing []frame, occurrence *calcOccurrence) (Value, error) { frame.host = calcStmtHost{ctx: ctx, shape: shape, self: self, occ: occurrence} - frame.env = stmtEnv{data: frame.locals(), enclosing: shape.bodyEnclosing(enclosing)} + frame.env = stmtEnv{ + data: frame.locals(), enclosing: shape.bodyEnclosing(enclosing), locals: make(map[string]Value), + } thisOccurrence := frame.engine.thisOccurrence if thisOccurrence == nil { thisOccurrence = frame.host.materializeOccurrence @@ -951,7 +955,11 @@ func (ctx *Context) runCalcBody(shape *calcShape, frame *invocationFrame, caller if err != nil { return Value{}, err } + frame.cells = frame.env.data.cells if returned { + if err := ctx.freezeBodyCells(frame.cells); err != nil { + return Value{}, err + } return result, nil } out, err := shape.designatedOutput() @@ -963,12 +971,20 @@ func (ctx *Context) runCalcBody(shape *calcShape, frame *invocationFrame, caller // through the same run bookkeeping a calc usage's outputs use. run := newCalcRun(shape, callerScope, self, frame.locals()) run.activation, run.perf, run.occurrence = activation, frame.host.performance(), occurrence + run.bodyFrames = append(run.bodyFrames, frame.env.localFrame().snapshot()) if len(enclosing) > 0 { run.outer = &EvalContext{ctx: ctx, scope: callerScope, self: self, frames: enclosing, trace: ctx.trace, activation: activation} } // The invocation already holds this evaluation's nesting feature value. run.onStack = true - return run.value(ctx, out) + result, err = run.value(ctx, out) + if err != nil { + return Value{}, err + } + if err := ctx.freezeBodyCells(frame.cells); err != nil { + return Value{}, err + } + return result, nil } // toolCalcResult resolves what an invocation of a tool-computed calc yields when @@ -1029,6 +1045,9 @@ func (shape *calcShape) designatedToolOutput(outputs map[string]Value) (calcOutp // calc's parameters on the way in and its locals on the way out, reporting // the value host took from a `return` and whether the body returned one. func runCalcSteps(engine *stmtEngine, host *calcStmtHost, steps []lower.Statement) (Value, bool, error) { + deriving := engine.ctx.deriving + engine.ctx.deriving = nil + defer func() { engine.ctx.deriving = deriving }() flow, err := engine.run(steps) if err != nil { return Value{}, false, err @@ -1136,7 +1155,11 @@ func (ec *EvalContext) bindCalcParameter( } if param.Default == nil { if param.IsSubject { - if value, ok := ec.ctx.enclosingSubject(shape, enclosing); ok { + value, ok, err := ec.ctx.enclosingSubject(shape, enclosing) + if err != nil { + return Value{}, "", err + } + if ok { return value, "enclosing subject", nil } return Value{}, "", shape.unboundSubject(param) diff --git a/internal/exec/runtime/montecarlo_case.go b/internal/exec/runtime/montecarlo_case.go index 3b0a958983..532ccdba74 100644 --- a/internal/exec/runtime/montecarlo_case.go +++ b/internal/exec/runtime/montecarlo_case.go @@ -235,7 +235,7 @@ func (ctx *Context) monteCarloObserved(run *calcRun) (Value, error) { return Value{}, fmt.Errorf("%w: %s declares no %s::%s to observe", ErrMonteCarloObserved, run.shape.Label, MonteCarloCaseFQN, monteCarloObserved) } - if value, ok := run.env.lookup(name); ok { + if value, ok := run.lookup(name); ok { return value, nil } return Value{Kind: ValNull}, nil @@ -523,9 +523,23 @@ func (r *MonteCarloRun) returnResults() error { enclosing = run.shape.bodyEnclosing(run.outer.enclosingRun(run.shape)) } engine := newStmtEngineIn(ctx, host, run.env, enclosing) + for _, body := range run.bodyFrames { + body.each(func(name string, value Value) { + if engine.env.locals == nil { + engine.env.locals = make(map[string]Value) + } + engine.env.locals[name] = value + }) + } defer engine.finish() host.readPerformance(engine, run.perf) result, returned, err := runCalcSteps(engine, host, r.results) + body := engine.env.localFrame().snapshot() + if len(run.bodyFrames) == 0 { + run.bodyFrames = []frame{body} + } else { + run.bodyFrames[len(run.bodyFrames)-1] = body + } if err != nil { return calcFrame(run.shape.Kind, run.shape.Name, fmt.Errorf("result: %w", err)) } diff --git a/internal/exec/runtime/robustness_action_step_multiplicity_test.go b/internal/exec/runtime/robustness_action_step_multiplicity_test.go index c2ce0828ad..5206725cb5 100644 --- a/internal/exec/runtime/robustness_action_step_multiplicity_test.go +++ b/internal/exec/runtime/robustness_action_step_multiplicity_test.go @@ -621,10 +621,10 @@ func TestActionStepMultiplicityExploreOmitsRepeatedLocals(t *testing.T) { if err != nil { t.Fatalf("explore shared writers: %v", err) } - if !exploration.Complete() || len(exploration.Outcomes) != 3 { - t.Fatalf("exploration = %s with %d outcomes, want three complete outcomes", exploration.Status(), len(exploration.Outcomes)) + if !exploration.Complete() || len(exploration.Outcomes) != 1 { + t.Fatalf("exploration = %s with %d outcomes, want one complete outcome", exploration.Status(), len(exploration.Outcomes)) } - want := map[string]bool{"c = 1": true, "c = 2": true, "c = 3": true} + want := map[string]bool{"c = 3": true} for _, explored := range exploration.Outcomes { got := explored.Outcome.String() if !want[got] { diff --git a/internal/exec/runtime/robustness_body_feature_bindings_test.go b/internal/exec/runtime/robustness_body_feature_bindings_test.go new file mode 100644 index 0000000000..93714b2da4 --- /dev/null +++ b/internal/exec/runtime/robustness_body_feature_bindings_test.go @@ -0,0 +1,558 @@ +package runtime + +import ( + "context" + "errors" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" +) + +// TestRuntimeRobustnessBodyFeatureBindings pins body-binding errors and lifetimes. +func TestRuntimeRobustnessBodyFeatureBindings(t *testing.T) { + t.Run("cycle", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + action run { + attribute left : Integer = right + 1; + attribute right : Integer = left + 1; + first start; + then action read { assign left := right; } + } + }`, "run") + exec, err := ctx.CreateActionExecutor(action) + if err == nil { + err = exec.RunToCompletion() + } + if !errors.Is(err, ErrCyclicFeatureValue) { + t.Fatalf("RunToCompletion = %v, want ErrCyclicFeatureValue", err) + } + }) + + t.Run("destroyed_object_read", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + part def Target { attribute n : Integer := 2; } + action run { + in ref target : Target; + attribute live : Integer = target.n; + first start; + then action read { assign live := live + 1; } + } + }`, "run") + idx := ctx.model.resolver.Index() + target, err := ctx.Instantiate(oneSymbol(t, idx, "test::Target")) + if err != nil { + t.Fatalf("Instantiate Target: %v", err) + } + exec, err := ctx.CreateActionExecutorWithInputs(action, nil, map[string]Value{"target": objectValue(target)}) + if err != nil { + t.Fatalf("CreateActionExecutorWithInputs: %v", err) + } + if err := ctx.destroy(target); err != nil { + t.Fatalf("destroy Target: %v", err) + } + if _, _, err := ctx.readBodyValue(exec.root.cells, exec.root.data, "live"); !errors.Is(err, ErrOccurrenceDestroyed) { + t.Fatalf("read live after destroying target = %v, want ErrOccurrenceDestroyed", err) + } + }) + + t.Run("results_report_destroyed_binding_error", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + part def Target { attribute n : Integer := 2; } + action run { + in ref target : Target; + attribute live : Integer = target.n; + first start; + then action read { assign live := live + 1; } + } + }`, "run") + idx := ctx.model.resolver.Index() + target, err := ctx.Instantiate(oneSymbol(t, idx, "test::Target")) + if err != nil { + t.Fatalf("Instantiate Target: %v", err) + } + exec, err := ctx.CreateActionExecutorWithInputs(action, nil, map[string]Value{"target": objectValue(target)}) + if err != nil { + t.Fatalf("CreateActionExecutorWithInputs: %v", err) + } + exec.SetBreakpoint("read") + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + if got := exec.PausedAt(); got != "read" { + t.Fatalf("PausedAt = %q, want read", got) + } + if err := ctx.destroy(target); err != nil { + t.Fatalf("destroy Target: %v", err) + } + results, err := exec.ResultsWithError() + if !errors.Is(err, ErrOccurrenceDestroyed) { + t.Fatalf("ResultsWithError error = %v, want ErrOccurrenceDestroyed", err) + } + if _, ok := results["live"]; ok { + t.Fatalf("ResultsWithError returned failed live binding: %s", FormatValue(results["live"])) + } + if _, ok := exec.Results()["live"]; ok { + t.Fatal("Results returned a failed live binding") + } + }) + + t.Run("terminate_freezes_root_binding", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + part def Target { attribute n : Integer := 2; } + action run { + in ref target : Target; + attribute live : Integer = target.n; + first start; + then terminate; + } + action update { + in ref target : Target; + first start; + then action write { assign target.n := 5; } + } + }`, "run") + idx := ctx.model.resolver.Index() + target, err := ctx.Instantiate(oneSymbol(t, idx, "test::Target")) + if err != nil { + t.Fatalf("Instantiate Target: %v", err) + } + exec, err := ctx.CreateActionExecutorWithInputs(action, nil, map[string]Value{"target": objectValue(target)}) + if err != nil { + t.Fatalf("CreateActionExecutorWithInputs: %v", err) + } + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + if _, err := ctx.ExecuteActionWithInputs( + oneSymbol(t, idx, "test::update"), + map[string]Value{"target": objectValue(target)}, + ); err != nil { + t.Fatalf("ExecuteActionWithInputs update: %v", err) + } + if got := FormatValue(exec.Results()["live"]); got != "2" { + t.Fatalf("live after target.n := 5 = %s, want termination-time value 2", got) + } + }) + + t.Run("state_space_stop_time_reads_tracking_binding", func(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "stop-time-binding.sysml", parseAndBuild(t, `package test { + private import ScalarValues::*; + private import SI::*; + private import VectorFunctions::*; + private import StateSpaceRepresentation::*; + private import StateSpaceIntegration::*; + action dyn : ContinuousStateSpaceDynamics, FixedStepDynamics { + in stop : DurationValue = 2 [s]; + in :>> input : Input = VectorOf((0.0)); + :>> stateSpace = VectorOf((1.0)); + :>> timeStep = 0.1 [s]; + :>> stopTime = stop; + calc :>> getDerivative { + in input : Input; + in stateSpace : StateSpace; + return : StateDerivative = (0.0 - 0.5) * stateSpace / 1 [s]; + } + calc :>> getOutput { + in input : Input; + in stateSpace : StateSpace; + return : Output = stateSpace; + } + } + }`)) + exec, err := ctx.CreateActionExecutor(oneSymbol(t, idx, "test::dyn")) + if err != nil { + t.Fatalf("CreateActionExecutor: %v", err) + } + stop, ok := exec.root.data[exec.root.key("stop")] + if !ok { + t.Fatal("initialized stop parameter is missing") + } + if stop.Kind != ValQuantity { + t.Fatalf("stop = %s, want a quantity", FormatValue(stop)) + } + step, ok := exec.root.data[exec.root.key("timeStep")] + if !ok { + t.Fatal("initialized timeStep is missing") + } + ctx.writeBodyValue(exec.root.cells, exec.root.data, exec.root.key("stop"), step) + if _, ok := exec.root.data[exec.root.key("stopTime")]; ok { + t.Fatal("writing stop did not invalidate its derived stopTime") + } + run := &stateSpaceRun{dyn: exec.dynamics.dyn} + if err := exec.readStep(run); err != nil { + t.Fatalf("readStep after writing stop: %v", err) + } + if !run.stops || run.stop != 0.1 { + t.Fatalf("readStep stop = %v (stops=%v), want 0.1 after binding rederivation", run.stop, run.stops) + } + }) + + t.Run("state_exit_invalidates_dependents", func(t *testing.T) { + ctx, machine := loadState(t, `package test { + private import ScalarValues::*; + state machine { + attribute source : Integer := 1; + attribute top : Integer = active.value; + entry; then active; + state active { attribute value : Integer = source; } + state done; + transition first active accept when source > 0 do assign source := 2 then done; + } + }`, "machine") + exec, err := ctx.CreateStateExecutor(machine) + if err != nil { + t.Fatalf("CreateStateExecutor: %v", err) + } + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + data, err := exec.StateDataWithError() + if err != nil { + if !errors.Is(err, ErrNoValue) && !errors.Is(err, ErrUninitializedFeatureValue) && !errors.Is(err, ErrUnresolvedReference) { + t.Fatalf("read top after state exit = %v, want a typed inactive-state value error", err) + } + return + } + if got := FormatValue(data["top"]); got != "" { + t.Fatalf("top after state exit = %s, want ", got) + } + }) + + t.Run("write_stops_tracking", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + action run { + attribute source : Integer := 1; + attribute live : Integer = source * 2; + out attribute result : Integer; + first start; + then action write { + assign live := 99; + assign source := 5; + assign result := live; + } + } + }`, "run") + exec, err := ctx.CreateActionExecutor(action) + if err != nil { + t.Fatalf("CreateActionExecutor: %v", err) + } + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + if got := FormatValue(exec.Results()["result"]); got != "99" { + t.Fatalf("result = %s, want 99 after the binding is written", got) + } + }) + + t.Run("read_only_body_features", func(t *testing.T) { + for _, tc := range []struct { + name string + decl string + want string + }{ + {"constant", "constant attribute locked : Integer = 1;", "locked is constant"}, + {"derived", "attribute source : Integer := 1; derived attribute locked : Integer = source * 2;", "locked is derived"}, + } { + t.Run(tc.name, func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + action run { + `+tc.decl+` + first start; + then action write { assign locked := 2; } + } + }`, "run") + exec, err := ctx.CreateActionExecutor(action) + if err != nil { + t.Fatalf("CreateActionExecutor: %v", err) + } + if err := exec.RunToCompletion(); !errors.Is(err, ErrReadOnlyFeature) { + t.Fatalf("RunToCompletion = %v, want ErrReadOnlyFeature (%s)", err, tc.want) + } + }) + } + }) + + t.Run("rederivation_obeys_step_budget", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + action run { + attribute source : Integer := 1; + attribute live : Integer = source + 1; + first start; + then action done; + } + }`, "run") + exec, err := ctx.CreateActionExecutor(action) + if err != nil { + t.Fatalf("CreateActionExecutor: %v", err) + } + ctx.writeBodyValue(exec.root.cells, exec.root.data, "source", integerValue(2)) + ctx.run.steps = ctx.maxSteps + if _, _, err := ctx.readBodyValue(exec.root.cells, exec.root.data, "live"); !errors.Is(err, ErrStepLimitExceeded) { + t.Fatalf("read live at exhausted budget = %v, want ErrStepLimitExceeded", err) + } + }) + + t.Run("snapshot_restore", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + action run { + attribute source : Integer := 1; + attribute live : Integer = source * 2; + out attribute result : Integer; + first start; + then action prepare { + assign source := 4; + assign result := live; + } + then action finish { + assign source := 5; + assign result := live; + } + } + }`, "run") + exec, err := ctx.CreateActionExecutor(action) + if err != nil { + t.Fatalf("CreateActionExecutor: %v", err) + } + exec.SetBreakpoint("finish") + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion to breakpoint: %v", err) + } + if got := exec.PausedAt(); got != "finish" { + t.Fatalf("PausedAt = %q, want finish", got) + } + snapshot, err := exec.Snapshot() + if err != nil { + t.Fatalf("Snapshot: %v", err) + } + defer snapshot.Release() + finish := func() { + t.Helper() + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + if got := FormatValue(exec.Results()["result"]); got != "10" { + t.Fatalf("result = %s, want 10 after source := 5", got) + } + } + finish() + snapshot.Restore() + if got := FormatValue(exec.Results()["live"]); got != "8" { + t.Fatalf("live after restore = %s, want the captured 8", got) + } + finish() + }) + + t.Run("held_image", func(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "body-bindings-image.sysml", parseAndBuild(t, `package test { + private import ScalarValues::*; + action def Run { + attribute source : Integer := 1; + attribute live : Integer = source * 2; + out attribute result : Integer; + first start; + then action prepare { + assign source := 4; + assign result := live; + } + then action finish { + assign source := 5; + assign result := live; + } + } + part def Host { perform action run : Run; } + }`)) + host, err := ctx.Instantiate(oneSymbol(t, idx, "test::Host")) + if err != nil { + t.Fatalf("Instantiate Host: %v", err) + } + behavior, ok := host.Behavior("run") + if !ok || behavior.Action == nil { + t.Fatal("Host has no run action") + } + behavior.Action.SetBreakpoint("finish") + if err := behavior.Action.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion to breakpoint: %v", err) + } + image := imageInto(t, ctx, host) + copied, ok := image.Instance(host.ID) + if !ok { + t.Fatalf("image has no Host #%d", host.ID) + } + copiedBehavior, ok := copied.Behavior("run") + if !ok || copiedBehavior.Action == nil { + t.Fatal("image has no run action") + } + if err := copiedBehavior.Action.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion of imaged action: %v", err) + } + if got := FormatValue(copiedBehavior.Action.Results()["result"]); got != "10" { + t.Fatalf("imaged result = %s, want 10 after lazy rederivation", got) + } + }) + + t.Run("held_image_restores_binding_self_name_mask", func(t *testing.T) { + idx, _, ctx := buildRuntimeWithLibraries(t, "body-binding-mask-image.sysml", parseAndBuild(t, `package test { + private import ScalarValues::*; + action def Work { + in z : Integer; + first start; + then action reader { + in z : Integer = z; + out attribute result : Integer; + first start; + action heard accept g : Integer; + action finish { assign result := z; } + done; + succession first start then heard; + succession first heard then finish; + succession first finish then done; + } + } + part def Host { + attribute z : Integer := 4; + perform action work : Work { in z = this.z; } + } + }`)) + host, err := ctx.Instantiate(oneSymbol(t, idx, "test::Host")) + if err != nil { + t.Fatalf("Instantiate Host: %v", err) + } + behavior, ok := host.Behavior("work") + if !ok || behavior.Action == nil { + t.Fatal("Host has no work action") + } + nine := Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValInt, Int: 9}} + ctx.PostMessage(Message{SignalType: "Integer", Object: host.ID, Value: &nine}) + behavior.Action.SetBreakpoint("finish") + if err := behavior.Action.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion to the nested finish: %v", err) + } + if got := behavior.Action.PausedAt(); got != "finish" { + t.Fatalf("PausedAt = %q, state = %v, want the nested reader's finish (behavior error: %v)", + got, behavior.Action.State(), behavior.Err) + } + image := imageInto(t, ctx, host) + copied, ok := image.Instance(host.ID) + if !ok { + t.Fatalf("image has no Host #%d", host.ID) + } + copiedBehavior, ok := copied.Behavior("work") + if !ok || copiedBehavior.Action == nil { + t.Fatal("image has no work action") + } + if err := copiedBehavior.Action.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion of imaged action: %v", err) + } + results, err := copiedBehavior.Action.ResultsWithError() + if err != nil { + t.Fatalf("ResultsWithError: %v", err) + } + result := results["reader.result"] + if result.Kind != ValSequence || result.Sequence() == nil || result.Sequence().Size() != 1 { + t.Fatalf("imaged reader result = %s, want one value from performer z", FormatValue(result)) + } + got, err := result.Sequence().At(0) + if err != nil || FormatValue(got) != "4" { + t.Fatalf("imaged reader result element = %s, %v; want 4", FormatValue(got), err) + } + }) + + t.Run("replay", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + action run { + attribute source : Integer := 1; + attribute live : Integer = source * 2; + out attribute result : Integer; + first start; + then action write { + assign source := 5; + assign result := live; + } + } + }`, "run") + fresh := func() (*Context, error) { return NewContext(ctx.Model(), 100000), nil } + replayed, err := ReplaySchedule(context.Background(), fresh, starterOf(action), Witness{}, ScheduleEnd) + if err != nil { + t.Fatalf("ReplaySchedule: %v", err) + } + if replayed.Err != nil { + t.Fatalf("replay run: %v", replayed.Err) + } + if got, err := replayed.FinalValue("result"); err != nil || got != "10" { + t.Fatalf("replayed result = %s, %v, want 10", got, err) + } + }) + + t.Run("action_body_calc_usage_remains_one_shot", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + calc def Twice { + in x : Integer; + out y : Integer = x * 2; + } + action run { + attribute source : Integer := 2; + calc value : Twice { in x = source; } + out attribute result : Integer; + first start; + then action write { + assign result := value.y; + assign source := 5; + assign result := value.y; + } + } + }`, "run") + exec, err := ctx.CreateActionExecutor(action) + if err != nil { + t.Fatalf("CreateActionExecutor: %v", err) + } + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + if got := FormatValue(exec.Results()["result"]); got != "4" { + t.Fatalf("calc usage result = %s, want its one-shot pin value 4", got) + } + }) + + t.Run("block_body_calc_usage_remains_one_shot", func(t *testing.T) { + ctx, action := loadAction(t, `package test { + private import ScalarValues::*; + calc def Rhs { + in x : Integer; + out y : Integer = x * 2; + } + action run { + attribute x : Integer := 2; + out attribute result : Integer; + first start; + then action write { + if true { + calc k1 : Rhs { in x = x; } + assign result := k1.y; + assign x := 5; + assign result := k1.y; + } + } + } + }`, "run") + exec, err := ctx.CreateActionExecutor(action) + if err != nil { + t.Fatalf("CreateActionExecutor: %v", err) + } + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + if got := FormatValue(exec.Results()["result"]); got != "4" { + t.Fatalf("block calc usage result = %s, want its one-shot pin value 4", got) + } + }) +} diff --git a/internal/exec/runtime/robustness_case_bindings_test.go b/internal/exec/runtime/robustness_case_bindings_test.go new file mode 100644 index 0000000000..bd4b7f467f --- /dev/null +++ b/internal/exec/runtime/robustness_case_bindings_test.go @@ -0,0 +1,69 @@ +package runtime + +import ( + "errors" + "strings" + "testing" +) + +// TestRuntimeRobustnessCaseBindings covers the frame a case's conditions read its +// run's bindings through: a case-local the run left declared without a value +// shadows the binding of the same name, so a condition naming it is undecided for +// the missing value rather than read through to the enclosing one. +func TestRuntimeRobustnessCaseBindings(t *testing.T) { + t.Run("a mark on a frame holding no unvalued names is kept", func(t *testing.T) { + var f frame + f.markUnvalued("x") + if !f.has("x") { + t.Fatal("frame does not declare x after markUnvalued") + } + if _, ok, err := f.read(&Context{}, "x"); ok || !errors.Is(err, ErrNoValue) { + t.Fatalf("read of unvalued x = %v, %v, want ErrNoValue", ok, err) + } + }) + + t.Run("an unvalued case-local shadowing a bound parameter is undecided, not read through", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + part def Ship { attribute cost : Real = 5.0; } + part ship : Ship; + analysis bounded { + subject s = ship; + in limit : Real = 10.0; + objective fits { require constraint { c < limit } } + out c : Real = s.cost; + } + }` + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, src)) + sym := oneSymbol(t, idx, "test::bounded") + run, err := ctx.calcUsageRun(NewEvalContextIn(ctx, nil, nil), sym) + if err != nil { + t.Fatal(err) + } + if _, err := run.outputValues(ctx); err != nil { + t.Fatal(err) + } + if value, ok := run.env.lookup("limit"); !ok || FormatValue(value) != "10.0" { + t.Fatalf("limit bound to %s, %v; want 10.0", FormatValue(value), ok) + } + // The body left limit declared and unvalued, as a constraint body's + // performance leaves a root declaration no step wrote. + run.bodyFrames = append(run.bodyFrames, frame{unvalued: map[string]bool{"limit": true}}) + + bindings := run.bindingsFrame(ctx) + if !bindings.has("limit") { + t.Error("bindings frame does not declare the unvalued limit") + } + if _, ok, err := bindings.read(ctx, "limit"); ok || !errors.Is(err, ErrNoValue) { + t.Errorf("read of unvalued limit = %v, %v, want ErrNoValue", ok, err) + } + + verdicts := ctx.analysisVerdicts(run, sym, nil) + if len(verdicts) != 1 || verdicts[0].Name != "fits" { + t.Fatalf("verdicts = %+v, want the one objective fits", verdicts) + } + if got := verdicts[0]; got.Status != VerdictUndecided || !strings.Contains(got.Detail, "no value for feature limit") { + t.Fatalf("objective fits = %s (%q), want undecided for the unvalued limit", got.Status, got.Detail) + } + }) +} diff --git a/internal/exec/runtime/robustness_constraint_body_steps_test.go b/internal/exec/runtime/robustness_constraint_body_steps_test.go index a9a9aeca46..b1202e197f 100644 --- a/internal/exec/runtime/robustness_constraint_body_steps_test.go +++ b/internal/exec/runtime/robustness_constraint_body_steps_test.go @@ -84,6 +84,49 @@ func TestRuntimeRobustnessConstraintBodySteps(t *testing.T) { } }) + t.Run("a root unvalued declaration is missing until written and reaches the verdict", func(t *testing.T) { + env := &stmtEnv{ + data: mapFrame(map[string]Value{"x": constInt(9)}), + locals: make(map[string]Value), + unvaluedLocal: map[string]bool{"x": true}, + } + ctx := &Context{} + if _, ok, err := env.localFrame().read(ctx, "x"); ok || !errors.Is(err, ErrNoValue) { + t.Fatalf("read of unvalued x = %v, %v, want ErrNoValue", ok, err) + } + if !env.assignLocal(nil, "x", constInt(5)) { + t.Fatal("write to root unvalued x did not bind it") + } + if value, ok, err := env.localFrame().read(ctx, "x"); err != nil || !ok || FormatValue(value) != "5" { + t.Fatalf("read of bound x = %s, %v, %v; want 5", FormatValue(value), ok, err) + } + + src := `package test { + private import ScalarValues::*; + part def Rig { + attribute x : Integer = 9; + constraint writes { + attribute x : Integer; + assign x := 5; + x == 5 + } + } + }` + 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") + } + satisfied, err := ctx.EvaluateConstraintOn(feat.Symbol, feat.DeclScope(), nil) + if err != nil { + t.Fatalf("err = %v", err) + } + if !satisfied { + t.Error("the constraint result must read the root local's bound value") + } + }) + t.Run("two checks of one body share no state", func(t *testing.T) { src := `package test { private import ScalarValues::*; diff --git a/internal/exec/runtime/shared_default.go b/internal/exec/runtime/shared_default.go index a7477c4579..c2e12d054a 100644 --- a/internal/exec/runtime/shared_default.go +++ b/internal/exec/runtime/shared_default.go @@ -189,7 +189,7 @@ func (ctx *Context) sharesDefault(inst *Instance, fv *FeatureValue) bool { // sharedShape is the shape fv's derived default on inst is shared under, nil when // sharesDefault does not hold. func (ctx *Context) sharedShape(inst *Instance, fv *FeatureValue) *shapeNode { - if !ctx.sharing() || !fv.Feature.Scalar() || fv.Written || fv.BindingDerived || + if fv.Feature == nil || !ctx.sharing() || !fv.Feature.Scalar() || fv.Written || fv.BindingDerived || ctx.behaviorRunDepth != 0 || ctx.defaultYieldsToSubsetters(inst, fv.Feature) { return nil } @@ -265,6 +265,10 @@ func (ctx *Context) observeRead(inst *Instance, fv *FeatureValue) { if !t.clean || t.fv == fv { continue } + if inst == nil || fv.Feature == nil { + t.clean = false + continue + } if !ctx.declaredWithin(inst, t.inst) { t.clean = false continue @@ -309,6 +313,9 @@ func (ctx *Context) sharedRecordOf(inst *Instance, fv *FeatureValue) (*sharedDef // materialize under it: held, at every step, by a declared composite value of an // unclassified object. func (ctx *Context) declaredWithin(inst, root *Instance) bool { + if inst == nil || root == nil { + return inst == root + } for inst != root { if len(inst.classifiers) != 0 { return false @@ -328,6 +335,9 @@ func (ctx *Context) declaredWithin(inst, root *Instance) bool { // singlyWithin reports whether inst is root itself or held under it by single-valued // features at every step, so a path of feature names from root names inst alone. func (ctx *Context) singlyWithin(inst, root *Instance) bool { + if inst == nil || root == nil { + return inst == root + } for inst != root { owner, feature := inst.Owner() if owner == nil { diff --git a/internal/exec/runtime/signal.go b/internal/exec/runtime/signal.go index 09a447e1ea..917a55961c 100644 --- a/internal/exec/runtime/signal.go +++ b/internal/exec/runtime/signal.go @@ -957,7 +957,10 @@ func (ec *EvalContext) boundTargetAddresses(send lower.Send) ([]messageAddress, if root == thisName { return nil, false, nil } - value, bound := ec.Lookup(root) + value, bound, err := ec.Lookup(root) + if err != nil { + return nil, false, err + } if !bound { return nil, false, nil } diff --git a/internal/exec/runtime/snapshot.go b/internal/exec/runtime/snapshot.go index 2570f8897a..6f05ad23aa 100644 --- a/internal/exec/runtime/snapshot.go +++ b/internal/exec/runtime/snapshot.go @@ -294,6 +294,8 @@ type bodyCapture struct { run *bodyRun saved bodyRun frames []bodyFrame + cells []bodyCellsCapture + locals []mapState[string, Value] } // captureBody captures the run and, into the set, the executors its paused work @@ -301,17 +303,44 @@ type bodyCapture struct { func (s *executorCaptures) captureBody(run *bodyRun) *bodyCapture { c := &bodyCapture{run: run, saved: *run} c.saved.work, c.saved.cursor, c.saved.resuming = run.work.clone(), nil, nil - for _, f := range run.cursor { - c.frames = append(c.frames, f.clone()) - switch f := f.(type) { - case *calleeFrame: - s.captureAction(f.exec) - case *caseStepFrame: - if f.run == nil { - s.captureAction(f.start.host.flow) + var captureFrames func([]bodyFrame) + captureFrames = func(frames []bodyFrame) { + for _, f := range frames { + switch f := f.(type) { + case *engineFrame: + if locals := f.engine.env.locals; locals != nil { + c.locals = append(c.locals, captureMap(locals)) + } + if cells := f.engine.env.localCells; cells != nil && !bodyCellsCaptured(c.cells, cells) { + c.cells = append(c.cells, captureBodyCells(cells)) + } + case *blockFrame: + if f.cells != nil && !bodyCellsCaptured(c.cells, f.cells) { + c.cells = append(c.cells, captureBodyCells(f.cells)) + } + case *loopFrame: + if f.cells != nil && !bodyCellsCaptured(c.cells, f.cells) { + c.cells = append(c.cells, captureBodyCells(f.cells)) + } + case *stmtListFrame: + for _, strand := range f.strands { + if strand != nil { + captureFrames(strand.cursor) + } + } + case *calleeFrame: + s.captureAction(f.exec) + case *caseStepFrame: + if f.run == nil { + s.captureAction(f.start.host.flow) + } } } } + captureFrames(run.cursor) + for _, f := range run.cursor { + c.frames = append(c.frames, f.clone()) + } if held := run.paused.wait.held; held != nil { s.captureAction(held) } @@ -322,6 +351,12 @@ func (s *executorCaptures) captureBody(run *bodyRun) *bodyCapture { } func (c *bodyCapture) restore() { + for _, locals := range c.locals { + locals.restore() + } + for _, cells := range c.cells { + cells.restore() + } *c.run = c.saved c.run.work = c.saved.work.clone() c.run.cursor = make([]bodyFrame, len(c.frames)) @@ -330,6 +365,16 @@ func (c *bodyCapture) restore() { } } +// bodyCellsCaptured reports whether a snapshot journals the given cell store. +func bodyCellsCaptured(captures []bodyCellsCapture, cells *bodyCells) bool { + for _, capture := range captures { + if capture.cells == cells { + return true + } + } + return false +} + // Release ends the snapshot: it is no longer restorable, and the journal keeps // nothing for it. Releasing twice does nothing. func (s *Snapshot) Release() { @@ -585,17 +630,25 @@ func (e *ActionExecutor) reachableFrames() []*actionFrame { // frameCapture is one performance's state by value; the maps it holds are // restored in place, so a frame reading one of them as its own sees the restored values. type frameCapture struct { - perf *actionFrame - saved actionFrame - locals []mapState[string, Value] - data mapState[string, Value] + perf *actionFrame + saved actionFrame + locals []mapState[string, Value] + data mapState[string, Value] + cells bodyCellsCapture + localCells []bodyCellsCapture } func captureFrame(perf *actionFrame) frameCapture { - c := frameCapture{perf: perf, saved: *perf, data: captureMap(perf.data)} + c := frameCapture{ + perf: perf, saved: *perf, data: captureMap(perf.data), + cells: captureBodyCells(perf.cells), + } for _, local := range perf.locals { c.locals = append(c.locals, captureMap(local)) } + for _, cells := range perf.localCells { + c.localCells = append(c.localCells, captureBodyCells(cells)) + } c.saved.outer = slices.Clone(perf.outer) c.saved.connections = slices.Clone(perf.connections) c.saved.features = maps.Clone(perf.features) @@ -621,6 +674,13 @@ func (c frameCapture) restore() { perf.locals = append(perf.locals, local.restore()) } perf.data = c.data.restore() + c.cells.restore() + perf.cells = c.cells.cells + perf.localCells = make([]*bodyCells, len(c.localCells)) + for i, cells := range c.localCells { + cells.restore() + perf.localCells[i] = cells.cells + } perf.outer = slices.Clone(c.saved.outer) perf.connections = slices.Clone(c.saved.connections) perf.features = maps.Clone(c.saved.features) @@ -717,6 +777,8 @@ type stateCapture struct { events eventHeap stateData mapState[string, Value] stateAttrs map[*ast.StateNode]mapState[string, Value] + stateCells bodyCellsCapture + stateAttrCells map[*ast.StateNode]bodyCellsCapture stateVisits []string stateStack []*ast.StateNode fired []FiredTransition @@ -774,6 +836,8 @@ func (e *StateExecutor) capture() stateCapture { nextEventID: e.nextEventID, stateData: captureMap(e.stateData), stateAttrs: make(map[*ast.StateNode]mapState[string, Value], len(e.stateAttrs)), + stateCells: captureBodyCells(e.stateCells), + stateAttrCells: make(map[*ast.StateNode]bodyCellsCapture, len(e.stateAttrCells)), stateVisits: slices.Clone(e.stateVisits), stateStack: slices.Clone(e.stateStack), fired: slices.Clone(e.fired), @@ -808,6 +872,9 @@ func (e *StateExecutor) capture() stateCapture { for node, attrs := range e.stateAttrs { c.stateAttrs[node] = captureMap(attrs) } + for node, cells := range e.stateAttrCells { + c.stateAttrCells[node] = captureBodyCells(cells) + } for node, record := range e.history { c.history[node] = historyRecord{child: record.child, regions: maps.Clone(record.regions)} } @@ -824,12 +891,19 @@ func (c stateCapture) restore() { e.eventQueue.events = slices.Clone(c.events) } e.stateData = c.stateData.restore() + c.stateCells.restore() + e.stateCells = c.stateCells.cells if e.stateAttrs != nil { clear(e.stateAttrs) for node, attrs := range c.stateAttrs { e.stateAttrs[node] = attrs.restore() } } + e.stateAttrCells = make(map[*ast.StateNode]*bodyCells, len(c.stateAttrCells)) + for node, cells := range c.stateAttrCells { + cells.restore() + e.stateAttrCells[node] = cells.cells + } e.stateVisits, e.stateStack = slices.Clone(c.stateVisits), slices.Clone(c.stateStack) e.fired, e.firedBase = slices.Clone(c.fired), c.firedBase e.breakpointHit, e.pausedAt, e.completionDue = c.breakpointHit, c.pausedAt, c.completionDue diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index ffe58db738..2f851fcc5b 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -53,11 +53,13 @@ type StateExecutor struct { // timer set for later is this machine's wait on the clock. eventQueue *EventQueue stateData map[string]Value // State machine local variables + stateCells *bodyCells // stateAttrs holds the attributes each state owns, one map per state node, so // two usages of one state definition keep separate values. - stateAttrs map[*ast.StateNode]map[string]Value - stateVisits []string // Ordered list of visited state names - stateStack []*ast.StateNode // Active state configuration (for nested states) + stateAttrs map[*ast.StateNode]map[string]Value + stateAttrCells map[*ast.StateNode]*bodyCells + stateVisits []string // Ordered list of visited state names + stateStack []*ast.StateNode // Active state configuration (for nested states) // history records, per composite state, the configuration that state had when // it was last exited. A history pseudostate re-enters that configuration @@ -255,6 +257,9 @@ func newStateExecutorForOccurrence( if err := exec.initializeStateAttributes(); err != nil { return nil, err } + if err := ctx.deriveBodyCells(exec.stateCells); err != nil { + return nil, fmt.Errorf("derive state machine attributes: %w", err) + } ctx.clock.attach(exec) return exec, nil @@ -279,6 +284,7 @@ func newStateExecutorOn( eventQueue: NewEventQueue(), stateData: make(map[string]Value), stateAttrs: make(map[*ast.StateNode]map[string]Value), + stateAttrCells: make(map[*ast.StateNode]*bodyCells), stateVisits: make([]string, 0), stateStack: make([]*ast.StateNode, 0), history: make(map[*ast.StateNode]*historyRecord), @@ -294,6 +300,25 @@ func newStateExecutorOn( entering: make(map[*ast.StateNode]bool), } exec.driven.exec = exec + for _, attr := range graph.Attributes { + if !attr.Binding || attr.Value == nil { + continue + } + scope := attr.Scope + if scope == nil { + scope = graph.Scope + } + var cell *bodyCell + onDerived := func(value *Value) error { + mirrored, err := exec.mirrorBindingOccurrence(attr.Name, *value, cell) + if err != nil { + return err + } + *value = mirrored + return nil + } + cell = ctx.registerBodyBinding(exec.ensureStateCells(), attr.Name, attr.Value, scope, nil, onDerived) + } return exec } @@ -327,8 +352,18 @@ func (e *StateExecutor) initializeAttributes() error { return fmt.Errorf("%w: read %s of object #%d: %w", ErrStatePerformanceOccurrence, attr.Name, e.occurrence.ID, err) } - if value := fv.HeldValue(); value.Kind != ValInvalid { - e.stateData[attr.Name] = value + if attr.Binding && !fv.Written && !occurrenceHasRedefinedDefault(fv, attr, e.graph.Scope) { + _, seeded, err := e.ctx.seedBodyBindingFromFeatureValue(e.stateCells, attr.Name, fv) + if err != nil { + return fmt.Errorf("derive state machine attribute %s: %w", attr.Name, err) + } + if seeded { + continue + } + } + if value := fv.HeldValue(); value.Kind != ValInvalid && + (!attr.Binding || fv.Written || occurrenceHasRedefinedDefault(fv, attr, e.graph.Scope)) { + e.ctx.writeBodyValue(e.stateCells, e.stateData, attr.Name, value) continue } } @@ -338,6 +373,9 @@ func (e *StateExecutor) initializeAttributes() error { } continue } + if attr.Binding { + continue + } value, err := evalDefaults().evalIn(attr.Scope).Eval(attr.Value) if err != nil { return fmt.Errorf("eval attribute default %s: %w", attr.Name, err) @@ -345,7 +383,7 @@ func (e *StateExecutor) initializeAttributes() error { if value, err = e.mirrorOccurrence(attr.Name, value); err != nil { return err } - e.stateData[attr.Name] = value + e.ctx.writeBodyValue(e.stateCells, e.stateData, attr.Name, value) } return nil @@ -374,14 +412,41 @@ func (e *StateExecutor) bindContextDefault(attr lower.Attribute) bool { if err != nil { return false } - e.stateData[attr.Name] = value + e.ctx.writeBodyValue(e.stateCells, e.stateData, attr.Name, value) return true } // dataFrame is the machine's data as a run frame of the machine, so `Ctl::context` // written in a member the machine owns reads the binding the running machine gave it. func (e *StateExecutor) dataFrame() frame { - return frame{vars: e.stateData, performed: e.stateMachine, machine: e} + return frame{ + vars: e.stateData, cells: e.stateCells, performed: e.stateMachine, machine: e, + ensureCells: e.ensureStateCells, + } +} + +// ensureStateCells lazily creates dependency cells for machine data. +func (e *StateExecutor) ensureStateCells() *bodyCells { + if e.stateCells == nil { + e.stateCells = newBodyCells(e.stateData, func(scope *symbols.Scope) *EvalContext { + ec := NewEvalContextIn(e.ctx, scope, e.self) + ec.thisOccurrence = e.materializeOccurrence + ec.pushFrame(e.dataFrame()) + return ec + }) + } + return e.stateCells +} + +// stateAttributeContext resolves an attribute in machine data and its state frames. +func (e *StateExecutor) stateAttributeContext(state *ast.StateNode, scope *symbols.Scope) *EvalContext { + ec := NewEvalContextIn(e.ctx, scope, e.self) + ec.thisOccurrence = e.materializeOccurrence + ec.pushFrame(e.dataFrame()) + for _, fr := range e.attrFramesFor(state) { + ec.pushFrame(fr) + } + return ec } // initializeStateAttributes gives every state that owns attributes its own @@ -393,6 +458,22 @@ func (e *StateExecutor) initializeStateAttributes() error { } data := make(map[string]Value, len(attrs)) e.stateAttrs[state] = data + var cells *bodyCells + for _, attr := range attrs { + if attr.Binding && attr.Value != nil { + if cells == nil { + cells = newBodyCells(data, func(scope *symbols.Scope) *EvalContext { + return e.stateAttributeContext(state, scope) + }) + e.stateAttrCells[state] = cells + } + scope := attr.Scope + if scope == nil { + scope = e.graph.Scope + } + e.ctx.registerBodyBinding(cells, attr.Name, attr.Value, scope, nil, nil) + } + } for _, attr := range attrs { if attr.Value == nil { continue @@ -401,17 +482,25 @@ func (e *StateExecutor) initializeStateAttributes() error { if scope == nil { scope = e.graph.Scope } - ec := NewEvalContextIn(e.ctx, scope, e.self) - ec.occurrence = e.occurrence - ec.thisOccurrence = e.materializeOccurrence - ec.pushFrame(e.dataFrame()) - end := ec.beginStep() - value, err := ec.Eval(attr.Value) - end() + var value Value + var err error + if attr.Binding { + value, err = e.ctx.deriveBodyCell(cells, attr.Name, cells.existingCell(attr.Name)) + } else { + ec := NewEvalContextIn(e.ctx, scope, e.self) + ec.occurrence = e.occurrence + ec.thisOccurrence = e.materializeOccurrence + ec.pushFrame(e.dataFrame()) + end := ec.beginStep() + value, err = ec.Eval(attr.Value) + end() + } if err != nil { return fmt.Errorf("eval attribute default %s of state %s: %w", attr.Name, state.Name, err) } - data[attr.Name] = value + if !attr.Binding { + e.ctx.writeBodyValue(cells, data, attr.Name, value) + } } } return nil @@ -419,15 +508,15 @@ func (e *StateExecutor) initializeStateAttributes() error { // attrFramesFor are the attribute values a behavior of state reads, outermost // state first so an inner state's attribute shadows an enclosing one's. -func (e *StateExecutor) attrFramesFor(state *ast.StateNode) []map[string]Value { +func (e *StateExecutor) attrFramesFor(state *ast.StateNode) []frame { if state == nil || len(e.stateAttrs) == 0 { return nil } - var frames []map[string]Value + var frames []frame chain := e.getParentChain(state) for i := len(chain) - 1; i >= 0; i-- { if data := e.stateAttrs[chain[i]]; data != nil { - frames = append(frames, data) + frames = append(frames, frame{vars: data, cells: e.stateAttrCells[chain[i]]}) } } return frames @@ -436,9 +525,9 @@ func (e *StateExecutor) attrFramesFor(state *ast.StateNode) []map[string]Value { // stateAttributeValues is the value map of the innermost state at or enclosing // state that owns an attribute of this name, with the scope that attribute is // declared in, so a write to it answers to its declaration. -func (e *StateExecutor) stateAttributeValues(state *ast.StateNode, name string) (map[string]Value, *symbols.Scope, bool) { +func (e *StateExecutor) stateAttributeValues(state *ast.StateNode, name string) (map[string]Value, *symbols.Scope, *bodyCells, bool) { if state == nil { - return nil, nil, false + return nil, nil, nil, false } for _, ancestor := range e.getParentChain(state) { data, ok := e.stateAttrs[ancestor] @@ -453,10 +542,10 @@ func (e *StateExecutor) stateAttributeValues(state *ast.StateNode, name string) if scope == nil { scope = e.graph.Scope } - return data, scope, true + return data, scope, e.stateAttrCells[ancestor], true } } - return nil, nil, false + return nil, nil, nil, false } func (e *StateExecutor) declaresAttribute(name string) bool { @@ -483,15 +572,22 @@ func (e *StateExecutor) materializeOccurrence() (*Instance, error) { if err != nil { return nil, err } + e.occurrence = inst for _, attr := range e.graph.Attributes { if value, held := e.stateData[attr.Name]; held { + if cell := e.stateCells.existingCell(attr.Name); cell != nil && cell.binding != nil && !cell.fv.Written { + if _, err := e.mirrorBindingOccurrence(attr.Name, value, cell); err != nil { + return nil, fmt.Errorf("%w: seed %s of object #%d: %w", + ErrStatePerformanceOccurrence, attr.Name, inst.ID, err) + } + continue + } if err := inst.BindFeatureValue(e.ctx, attr.Name, value); err != nil { return nil, fmt.Errorf("%w: seed %s of object #%d: %w", ErrStatePerformanceOccurrence, attr.Name, inst.ID, err) } } } - e.occurrence = inst e.ctx.beginPerformanceLife(inst, e.ctx.newActivation()) return inst, nil } @@ -515,6 +611,19 @@ func (e *StateExecutor) mirrorOccurrence(name string, value Value) (Value, error return fv.HeldValue(), nil } +// mirrorBindingOccurrence exposes a tracked state feature through its occurrence. +func (e *StateExecutor) mirrorBindingOccurrence(name string, value Value, cell *bodyCell) (Value, error) { + if e.occurrence == nil || !e.declaresAttribute(name) { + return value, nil + } + mirrored, err := e.ctx.mirrorBodyCell(e.occurrence, name, cell, value) + if err != nil { + return value, fmt.Errorf("%w: write %s of object #%d: %w", + ErrStatePerformanceOccurrence, name, e.occurrence.ID, err) + } + return mirrored, nil +} + func (e *StateExecutor) assignAttribute(name string, value Value) error { if e.occurrence != nil { if err := e.occurrence.SetFeatureValue(e.ctx, name, value); err != nil { @@ -538,7 +647,7 @@ func (e *StateExecutor) assignAttribute(name string, value Value) error { return err } } - e.stateData[name] = value + e.ctx.writeBodyValue(e.stateCells, e.stateData, name, value) return nil } @@ -577,7 +686,7 @@ func (e *StateExecutor) evalStepWithin(owner ast.Node, f *firing, node ast.Node, ec.pushFrame(data) if state, ok := owner.(*ast.StateNode); ok { for _, frame := range e.attrFramesFor(state) { - ec.Push(frame) + ec.pushFrame(frame) } } defer ec.beginStep()() @@ -2103,17 +2212,20 @@ func orAnonymousSignal(signalType string) string { func (e *StateExecutor) restoreSharedData(names []string) func() { saved := make(map[string]Value, len(names)) held := make(map[string]bool, len(names)) + cells := make(map[string]bodyCellState, len(names)) for _, name := range names { value, ok := e.stateData[name] saved[name], held[name] = value, ok + cells[name] = bodyCellStateOf(e.stateCells, name) } return func() { for name, wasHeld := range held { if wasHeld { - e.stateData[name] = saved[name] + e.ctx.writeBodyValue(e.stateCells, e.stateData, name, saved[name]) } else { - delete(e.stateData, name) + e.ctx.clearBodyValue(e.stateCells, e.stateData, name) } + e.ctx.restoreBodyCellState(e.stateCells, name, cells[name]) } } } @@ -2379,6 +2491,9 @@ func (e *StateExecutor) completeMachine() error { if err := e.exitMachine(); err != nil { return err } + if err := e.ctx.freezeBodyCells(e.stateCells); err != nil { + return err + } e.state = StateCompleted e.ctx.endPerformanceLife(e.occurrence) return nil @@ -2388,6 +2503,9 @@ func (e *StateExecutor) completeMachine() error { // transition reached (SysML v2 §7.18.3): no state is exited and no exit behavior // runs; the do behaviors under way are abandoned, and no state stays active. func (e *StateExecutor) terminateMachine(fromName string, trigger ast.Node, stop *ast.Usage) error { + if err := e.ctx.freezeBodyCells(e.stateCells); err != nil { + return err + } name, _ := ast.EffectiveName(stop) if e.trace() != nil { e.trace().RecordStateTransition(e.traceOrigin(), fromName, name, triggerName(trigger)) @@ -5248,6 +5366,7 @@ func (e *StateExecutor) resetEntering() { // performEntry records the entry of state and performs its entry behaviors. func (e *StateExecutor) performEntry(state *ast.StateNode) error { + e.ctx.restartBodyCells(e.stateAttrCells[state]) if !e.activeAtEntry[state] { e.entering[state] = true } @@ -5413,6 +5532,10 @@ func (e *StateExecutor) exitState(state *ast.StateNode) error { if err := e.performingBehaviors(state, e.behaviorsOf(state).Exit); err != nil { return fmt.Errorf("exit action: %w", err) } + if err := e.ctx.freezeBodyCells(e.stateAttrCells[state]); err != nil { + return fmt.Errorf("freeze state %s attributes: %w", state.Name, err) + } + e.ctx.exitBodyCells(e.stateAttrCells[state]) // Clear simple state e.activeConfig.simpleState = nil @@ -5448,6 +5571,9 @@ func (e *StateExecutor) exitRegionsBelow(owner *ast.StateNode, regions []*ast.St // passing state data in through the callee's input parameters and merging its // output parameters back into state data. func (e *StateExecutor) invokeNested(inv actionInvocation) error { + if err := e.ctx.deriveBodyCells(e.stateCells); err != nil { + return err + } _, outputs, err := invokeAction(e.ctx, e.stateMachine.Scope, inv, e.stateData, e.self) if err != nil { return err @@ -5468,7 +5594,7 @@ func (e *StateExecutor) writeStateValue(name string, value Value) error { if e.declaresAttribute(name) { return e.assignAttribute(name, value) } - e.stateData[name] = value + e.ctx.writeBodyValue(e.stateCells, e.stateData, name, value) return nil } @@ -5582,17 +5708,46 @@ func (e *StateExecutor) StateStack() []*ast.StateNode { // attributes each state owns under that state's path (`nested.hits`), which two // usages of one state definition hold separately. func (e *StateExecutor) StateData() map[string]Value { - data := make(map[string]Value, len(e.stateData)) - for k, v := range e.stateData { - data[k] = v + data, _ := e.StateDataWithError() + return data +} + +// StateDataWithError returns state data after deriving current tracking cells. +// A value whose inactive-state reference cannot be read is omitted and reported. +func (e *StateExecutor) StateDataWithError() (map[string]Value, error) { + var deriveErr error + if err := e.ctx.deriveBodyCells(e.stateCells); err != nil { + deriveErr = err } + data := copyStateValues(e.stateData, e.stateCells, "") for state, attrs := range e.stateAttrs { - prefix := e.statePath(state) + "." - for name, value := range attrs { - data[prefix+name] = value + cells := e.stateAttrCells[state] + if err := e.ctx.deriveBodyCells(cells); err != nil && deriveErr == nil { + deriveErr = err } + data = copyStateValuesInto(data, attrs, cells, e.statePath(state)+".") } - return data + return data, deriveErr +} + +// copyStateValues copies a state's materialized values under its qualified path. +func copyStateValues(values map[string]Value, cells *bodyCells, prefix string) map[string]Value { + return copyStateValuesInto(make(map[string]Value, len(values)), values, cells, prefix) +} + +// copyStateValuesInto adds a state's materialized values to an output map. +func copyStateValuesInto(target, values map[string]Value, cells *bodyCells, prefix string) map[string]Value { + for name, value := range values { + target[prefix+name] = value + } + if cells != nil { + for name, cell := range cells.cells { + if cell.binding != nil && !cell.fv.Written && !cell.binding.frozen && !cell.fv.Materialized { + delete(target, prefix+name) + } + } + } + return target } // statePath is a state's name qualified by the states enclosing it. diff --git a/internal/exec/runtime/state_space.go b/internal/exec/runtime/state_space.go index 91e27e4082..43d1df9025 100644 --- a/internal/exec/runtime/state_space.go +++ b/internal/exec/runtime/state_space.go @@ -132,7 +132,10 @@ func (e *ActionExecutor) initializeDynamics() error { func (e *ActionExecutor) readStep(run *stateSpaceRun) error { dyn := run.dyn what := fmt.Sprintf("%s of action %s", lower.TimeStepFeature, symbolText(e.action)) - step, ok := e.root.data[e.root.key(lower.TimeStepFeature)] + step, ok, err := e.ctx.readBodyValue(e.root.cells, e.root.data, e.root.key(lower.TimeStepFeature)) + if err != nil { + return fmt.Errorf("%w: %s: %w", ErrStateSpaceStep, what, err) + } if dyn.TimeStep == nil || !ok { return fmt.Errorf("%w: action %s states no %s; specialize %s or declare one", ErrStateSpaceStep, symbolText(e.action), lower.TimeStepFeature, lower.FixedStepDynamicsFQN) @@ -148,11 +151,14 @@ func (e *ActionExecutor) readStep(run *stateSpaceRun) error { if dyn.StopTime == nil { return nil } - stop, ok := e.root.data[e.root.key(lower.StopTimeFeature)] + what = fmt.Sprintf("%s of action %s", lower.StopTimeFeature, symbolText(e.action)) + stop, ok, err := e.ctx.readBodyValue(e.root.cells, e.root.data, e.root.key(lower.StopTimeFeature)) + if err != nil { + return fmt.Errorf("%w: %s: %w", ErrStateSpaceStep, what, err) + } if !ok || stop.Kind == ValNull { return nil } - what = fmt.Sprintf("%s of action %s", lower.StopTimeFeature, symbolText(e.action)) if run.stop, err = e.ctx.timeMagnitude(stop, what); err != nil { return fmt.Errorf("%w: %w", ErrStateSpaceStep, err) } @@ -165,7 +171,11 @@ func (e *ActionExecutor) readStep(run *stateSpaceRun) error { // stateVector is the state the action holds, checked to be a vector. func (e *ActionExecutor) stateVector() (Value, error) { - state, ok := e.root.data[e.root.key(lower.StateSpaceFeature)] + state, ok, err := e.ctx.readBodyValue(e.root.cells, e.root.data, e.root.key(lower.StateSpaceFeature)) + if err != nil { + return Value{}, fmt.Errorf("%w: action %s: read %s: %w", + ErrStateSpaceValue, symbolText(e.action), lower.StateSpaceFeature, err) + } if !ok || state.Kind == ValNull { return Value{}, fmt.Errorf("%w: action %s binds no %s; give it an initial value", ErrStateSpaceValue, symbolText(e.action), lower.StateSpaceFeature) @@ -184,7 +194,11 @@ func (e *ActionExecutor) dynamicsSteps() int { // inputVector is the input the action holds; nil when it binds none, so a calc // that does not need one runs and one that does reports its parameter unbound. func (e *ActionExecutor) inputVector() (*Value, error) { - input, ok := e.root.data[e.root.key(lower.InputFeature)] + input, ok, err := e.ctx.readBodyValue(e.root.cells, e.root.data, e.root.key(lower.InputFeature)) + if err != nil { + return nil, fmt.Errorf("%w: action %s: read %s: %w", + ErrStateSpaceValue, symbolText(e.action), lower.InputFeature, err) + } if !ok || input.Kind == ValNull { return nil, nil } diff --git a/internal/exec/runtime/state_space_accuracy_test.go b/internal/exec/runtime/state_space_accuracy_test.go index 5d311f5617..50c7d03537 100644 --- a/internal/exec/runtime/state_space_accuracy_test.go +++ b/internal/exec/runtime/state_space_accuracy_test.go @@ -30,6 +30,27 @@ func TestStateSpaceIntegratorsAgainstClosedForm(t *testing.T) { } } +func TestStateSpaceRealBindingUsesDeclaredReal(t *testing.T) { + const fixture = "state_space_euler_first_order.sysml" + src, err := os.ReadFile(filepath.Join("testdata", "conformance", fixture)) + if err != nil { + t.Fatal(err) + } + idx, _, ctx := buildRuntimeWithLibraries(t, fixture, parseAndBuild(t, string(src))) + sym := findSymbolByName(idx.DocumentRoot(fixture), "decay", ast.DefAction) + exec, err := ctx.CreateActionExecutor(sym) + if err != nil { + t.Fatal(err) + } + if err := exec.initialize(); err != nil { + t.Fatal(err) + } + rate := exec.root.data[exec.root.key("rate")] + if rate.Kind != ValConst || rate.Const.Kind != semantics.ValReal { + t.Fatalf("rate = %s, want a declared Real", FormatValue(rate)) + } +} + // stateSpaceFinalState runs the conformance fixture's action test::decay to // completion and returns the one component of its final state. func stateSpaceFinalState(t *testing.T, fixture string) float64 { diff --git a/internal/exec/runtime/state_statements.go b/internal/exec/runtime/state_statements.go index aba2e3db2d..4ba9de1c6e 100644 --- a/internal/exec/runtime/state_statements.go +++ b/internal/exec/runtime/state_statements.go @@ -22,7 +22,7 @@ type stateStmtHost struct { perfs *performances // attrs are the attributes of the state the behavior belongs to and of the // states enclosing it, innermost first. - attrs []map[string]Value + attrs []frame // firing is the transition the behavior reads as being taken: the one under way // for an entry, exit or effect, the one that entered the state for a do behavior. firing *firing @@ -145,7 +145,9 @@ func (e *StateExecutor) firingOf(t *lower.Transition) *firing { // name — `Track::context` inside a behavior of a state of Track's — finds its value // here. func (h *stateStmtHost) dataFrame() frame { - return frame{vars: h.exec.stateData, performed: h.exec.stateMachine, firing: h.firing, machine: h.exec} + fr := h.exec.dataFrame() + fr.firing = h.firing + return fr } // run executes the behavior's statements; a do behavior's pause where they wait @@ -299,7 +301,7 @@ func (run *doRun) visibleArmedWaits() []ClockWait { // rootFrame is the performance of the behavior itself: it holds no feature of its // own, and reads the machine's data and the enclosing states' attributes around it. -func (h *stateStmtHost) rootFrame(attrs []map[string]Value) *actionFrame { +func (h *stateStmtHost) rootFrame(attrs []frame) *actionFrame { root := &actionFrame{ scope: h.behavior.Scope, connections: h.exec.graph.Connections, @@ -315,9 +317,7 @@ func (h *stateStmtHost) rootFrame(attrs []map[string]Value) *actionFrame { if root.scope == nil { root.scope = h.exec.stateMachine.Scope } - for _, attr := range attrs { - root.outer = append(root.outer, mapFrame(attr)) - } + root.outer = append(root.outer, attrs...) return root } @@ -392,14 +392,14 @@ func (h *stateStmtHost) assignForeign(ec *EvalContext, s lower.Assign, value Val // behavior, or by one enclosing it, and reports whether it did. The value // answers to the attribute's declaration as every other write does. func (h *stateStmtHost) assignStateAttribute(name string, value Value) (bool, error) { - data, scope, ok := h.exec.stateAttributeValues(h.behavior.Owner, name) + data, scope, cells, ok := h.exec.stateAttributeValues(h.behavior.Owner, name) if !ok { return false, nil } if err := h.exec.ctx.checkNamedWrite(scope, h.describe(), name, &value); err != nil { return true, err } - data[name] = value + h.exec.ctx.writeBodyValue(cells, data, name, value) return true, nil } @@ -500,6 +500,7 @@ func (h *stateStmtHost) runFlow(block lower.Block) (stmtFlow, error) { root.scope = block.Graph.Scope } h.flow.declareRootFeatures(root) + h.flow.registerRootBindings(root) h.flow.declareAcceptPayloads(root) if err := h.flow.initializeAttributes(); err != nil { return flowNext, fmt.Errorf("%s: %w", h.describe(), err) @@ -514,7 +515,7 @@ func (h *stateStmtHost) setFeature(name string, value Value) error { if err := h.exec.ctx.checkNamedWrite(root.scope, h.describe(), name, &value); err != nil { return err } - root.data[root.key(name)] = value + h.exec.ctx.writeBodyValue(root.cells, root.data, root.key(name), value) return nil } if written, err := h.assignAround(name, value); written || err != nil { @@ -533,7 +534,7 @@ func (h *stateStmtHost) assignAround(name string, value Value) (bool, error) { return true, h.exec.assignAttribute(name, value) } if _, ok := h.exec.stateData[name]; ok { - h.exec.stateData[name] = value + h.exec.ctx.writeBodyValue(h.exec.stateCells, h.exec.stateData, name, value) return true, nil } return assignPerformerFeature(h.exec.ctx, h.exec.self, h.behavior.Scope, name, value) @@ -542,7 +543,7 @@ func (h *stateStmtHost) assignAround(name string, value Value) (bool, error) { // returnAround writes a returned output as assignAround does and keeps it for the // caller when a call event's transition is firing. func (h *stateStmtHost) returnAround(name string, value Value) (bool, error) { - if _, scope, ok := h.exec.stateAttributeValues(h.behavior.Owner, name); ok { + if _, scope, _, ok := h.exec.stateAttributeValues(h.behavior.Owner, name); ok { what := func() string { return fmt.Sprintf("%s: output %s returned", h.describe(), name) } if err := h.exec.ctx.checkMutable(scope, what, name); err != nil { return true, err diff --git a/internal/exec/runtime/state_unit_front.go b/internal/exec/runtime/state_unit_front.go index 63cd57ef79..547ea3234c 100644 --- a/internal/exec/runtime/state_unit_front.go +++ b/internal/exec/runtime/state_unit_front.go @@ -180,6 +180,7 @@ type firingScope struct { type dataSlot struct { value Value held bool + cell bodyCellState } // firing captures the firing the executor is in the middle of. @@ -203,8 +204,10 @@ func (e *StateExecutor) setFiring(s *firingScope) { func (s *firingScope) install(e *StateExecutor) { for name, value := range s.bound { prior, held := e.stateData[name] - s.shadowed[name] = dataSlot{value: prior, held: held} - e.stateData[name] = value + s.shadowed[name] = dataSlot{ + value: prior, held: held, cell: bodyCellStateOf(e.stateCells, name), + } + e.ctx.writeBodyValue(e.stateCells, e.stateData, name, value) } } @@ -218,11 +221,13 @@ func (s *firingScope) uninstall(e *StateExecutor) { // restore puts back what the data held under the firing's binding of name. func (s *firingScope) restore(e *StateExecutor, name string) { - if slot := s.shadowed[name]; slot.held { - e.stateData[name] = slot.value + slot := s.shadowed[name] + if slot.held { + e.ctx.writeBodyValue(e.stateCells, e.stateData, name, slot.value) } else { - delete(e.stateData, name) + e.ctx.clearBodyValue(e.stateCells, e.stateData, name) } + e.ctx.restoreBodyCellState(e.stateCells, name, slot.cell) } // runningQueue is the queue whose units are running, nil outside a front. @@ -235,7 +240,7 @@ func (e *StateExecutor) runningQueue() *unitQueue { // bindData binds one of the machine's data for the firing under way to read. func (e *StateExecutor) bindData(name string, value Value) { - e.stateData[name] = value + e.ctx.writeBodyValue(e.stateCells, e.stateData, name, value) if q := e.runningQueue(); q != nil { q.firing.bound[name] = value } @@ -254,7 +259,9 @@ func (e *StateExecutor) restoreData(names []string) func() { continue } prior, held := e.stateData[name] - s.shadowed[name] = dataSlot{value: prior, held: held} + s.shadowed[name] = dataSlot{ + value: prior, held: held, cell: bodyCellStateOf(e.stateCells, name), + } s.bound[name] = prior } return func() { diff --git a/internal/exec/runtime/statement_frame_state_test.go b/internal/exec/runtime/statement_frame_state_test.go new file mode 100644 index 0000000000..a28c813e2d --- /dev/null +++ b/internal/exec/runtime/statement_frame_state_test.go @@ -0,0 +1,79 @@ +package runtime + +import ( + "strings" + "testing" +) + +func TestSnapshotRestoresBodyCellsInUnorderedStrands(t *testing.T) { + rootValues := map[string]Value{"root": {Kind: ValNull}} + rootCells := newBodyCells(rootValues, nil) + rootCell := rootCells.cell("root") + rootCell.binding = &bodyBinding{frozen: true} + rootBinding := rootCell.binding + + blockValues := map[string]Value{"block": {Kind: ValNull}} + blockCells := newBodyCells(blockValues, nil) + blockCell := blockCells.cell("block") + blockCell.binding = &bodyBinding{frozen: true} + blockBinding := blockCell.binding + + engine := &engineFrame{engine: &stmtEngine{env: &stmtEnv{ + locals: rootValues, localCells: rootCells, + }}} + block := &blockFrame{locals: blockValues, cells: blockCells} + run := &bodyRun{ + work: &statementWork{}, + cursor: []bodyFrame{&stmtListFrame{strands: []*stmtStrand{{ + cursor: []bodyFrame{engine, block}, + }}}}, + } + + capture := (&executorCaptures{}).captureBody(run) + delete(rootValues, "root") + rootCell.binding.frozen = false + delete(blockValues, "block") + blockCell.binding.frozen = false + + capture.restore() + + if value, ok := rootValues["root"]; !ok || value.Kind != ValNull { + t.Fatalf("restored root local = %v, present %t; want null", value, ok) + } + if rootCell.binding == rootBinding || !rootCell.binding.frozen { + t.Fatalf("restored root binding = %+v, same pointer %t; want a frozen clone", + rootCell.binding, rootCell.binding == rootBinding) + } + if value, ok := blockValues["block"]; !ok || value.Kind != ValNull { + t.Fatalf("restored block local = %v, present %t; want null", value, ok) + } + if blockCell.binding == blockBinding || !blockCell.binding.frozen { + t.Fatalf("restored block binding = %+v, same pointer %t; want a frozen clone", + blockCell.binding, blockCell.binding == blockBinding) + } +} + +func TestStatementFrameStateSpellingIncludesBodyBindingState(t *testing.T) { + locals := map[string]Value{"value": {Kind: ValNull}} + cells := newBodyCells(locals, nil) + cell := cells.cell("value") + cell.binding = &bodyBinding{} + speller := &stateSpeller{} + + block := (&blockFrame{locals: locals, cells: cells}).spell(speller) + if !strings.Contains(block, "[tracking]") { + t.Fatalf("block state = %q, want tracking binding state", block) + } + cell.fv.Written = true + block = (&blockFrame{locals: locals, cells: cells}).spell(speller) + if !strings.Contains(block, "[written]") { + t.Fatalf("block state = %q, want written binding state", block) + } + + engine := (&engineFrame{engine: &stmtEngine{env: &stmtEnv{ + locals: locals, localCells: cells, + }}}).spell(speller) + if !strings.Contains(engine, "[written]") { + t.Fatalf("engine state = %q, want written binding state", engine) + } +} diff --git a/internal/exec/runtime/statements.go b/internal/exec/runtime/statements.go index 39f24c0c69..78fff53a07 100644 --- a/internal/exec/runtime/statements.go +++ b/internal/exec/runtime/statements.go @@ -1,6 +1,7 @@ package runtime import ( + "errors" "fmt" "maps" "slices" @@ -11,9 +12,8 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) -// stmtEnv is the environment a body's statements execute in: the behavior's own -// data, plus one frame per body-local block entered. A frame is discarded when -// its block exits, so a name it declares never leaks outward. +// stmtEnv is the environment a body's statements execute in: behavior data, root +// locals, and one frame per body-local block entered. type stmtEnv struct { data frame // enclosing are the frames the behavior's data shadows but its statements @@ -21,27 +21,47 @@ type stmtEnv struct { enclosing []frame // outer are value maps the body reads but does not declare into, innermost // last: the attributes the states enclosing the behavior own. - outer []map[string]Value + outer []frame // perf is the performance the body runs as where data is not its frame — a state // behavior's — whose nodes a read of `p.v` names. nil where data is the frame. - perf *actionFrame - frames []map[string]Value + perf *actionFrame + locals map[string]Value + localCells *bodyCells + unvaluedLocal map[string]bool + frames []map[string]Value + cells []*bodyCells // unvalued names, per frame, the features a frame declares but holds no value // for yet (`out v : Integer;`), which an assignment writes into that frame. unvalued []map[string]bool } // enter pushes a frame for a block about to run and returns it. -func (env *stmtEnv) enter() map[string]Value { +func (env *stmtEnv) enter(e *stmtEngine) map[string]Value { frame := make(map[string]Value) env.frames = append(env.frames, frame) + env.cells = append(env.cells, nil) env.unvalued = append(env.unvalued, nil) return frame } +// ensureLocalCells lazily creates dependency cells for an entered block. +func (e *stmtEngine) ensureLocalCells(index int) *bodyCells { + if e.env.cells[index] == nil { + depth := index + 1 + e.env.cells[index] = newBodyCells(e.env.frames[index], func(scope *symbols.Scope) *EvalContext { + return e.evalInDepth(scope, depth) + }) + } + return e.env.cells[index] +} + // leave discards the frame the innermost entered block declares into. -func (env *stmtEnv) leave() { +func (env *stmtEnv) leave(ctx *Context, forget bool) { if len(env.frames) > 0 { + if forget { + ctx.forgetBodyCells(env.cells[len(env.cells)-1]) + } + env.cells = env.cells[:len(env.cells)-1] env.frames = env.frames[:len(env.frames)-1] env.unvalued = env.unvalued[:len(env.unvalued)-1] } @@ -54,7 +74,10 @@ func (env *stmtEnv) leave() { func (env *stmtEnv) declareUnvalued(name string) { depth := len(env.frames) if depth == 0 { - env.data.markUnvalued(name) + if env.unvaluedLocal == nil { + env.unvaluedLocal = make(map[string]bool) + } + env.unvaluedLocal[name] = true return } if env.unvalued[depth-1] == nil { @@ -73,12 +96,67 @@ func (env *stmtEnv) frameDeclares(i int, name string) bool { // declare binds a name the innermost entered block declares, or a name of the // behavior's own data when no block is entered. -func (env *stmtEnv) declare(name string, value Value) { +func (env *stmtEnv) declare(ctx *Context, name string, value Value) { if depth := len(env.frames); depth > 0 { - env.frames[depth-1][name] = value + delete(env.unvalued[depth-1], name) + if cells := env.cells[depth-1]; cells != nil { + ctx.writeBodyCell(cells, name, value) + } else { + env.frames[depth-1][name] = value + } return } - env.data.set(name, value) + delete(env.unvaluedLocal, name) + if env.localCells != nil { + ctx.writeBodyCell(env.localCells, name, value) + } else { + env.locals[name] = value + } +} + +// ensureRootCells lazily creates dependency cells for behavior-local declarations. +func (e *stmtEngine) ensureRootCells() *bodyCells { + if e.env.localCells == nil { + e.env.localCells = newBodyCells(e.env.locals, func(scope *symbols.Scope) *EvalContext { + return e.evalInDepth(scope, 0) + }) + } + return e.env.localCells +} + +// rootDeclares reports whether the behavior's root scope declares name. +func (env *stmtEnv) rootDeclares(name string) bool { + if _, ok := env.locals[name]; ok { + return true + } + return env.unvaluedLocal[name] +} + +// rootLocal reports whether name belongs to the behavior root rather than a block. +func (env *stmtEnv) rootLocal(name string) bool { + if !env.rootDeclares(name) { + return false + } + for i := len(env.frames) - 1; i >= 0; i-- { + if env.frameDeclares(i, name) { + return false + } + } + return true +} + +// assignRootLocal writes a name declared in the behavior's root scope. +func (env *stmtEnv) assignRootLocal(ctx *Context, name string, value Value) bool { + if !env.rootDeclares(name) { + return false + } + delete(env.unvaluedLocal, name) + if env.localCells != nil { + ctx.writeBodyCell(env.localCells, name, value) + } else { + env.locals[name] = value + } + return true } // holdsLocal reports whether an entered block declares name. @@ -88,45 +166,126 @@ func (env *stmtEnv) holdsLocal(name string) bool { return true } } - return false + return env.rootDeclares(name) } -// assignLocal writes to the innermost entered block that declares name and -// reports whether one did: a block-local declaration shadows a name of the -// behavior's own, including one of its output features. -func (env *stmtEnv) assignLocal(name string, value Value) bool { +// assignLocal writes to the innermost local declaration of name and reports +// whether one did: a local shadows a name of the behavior's own. +func (env *stmtEnv) assignLocal(ctx *Context, name string, value Value) bool { for i := len(env.frames) - 1; i >= 0; i-- { if env.frameDeclares(i, name) { - env.frames[i][name] = value + delete(env.unvalued[i], name) + if cells := env.cells[i]; cells != nil { + ctx.writeBodyCell(cells, name, value) + } else { + env.frames[i][name] = value + } return true } } + if env.rootDeclares(name) { + delete(env.unvaluedLocal, name) + if env.localCells != nil { + ctx.writeBodyCell(env.localCells, name, value) + } else { + env.locals[name] = value + } + return true + } return false } -// values is the values a statement reads: the enclosing frames, overridden by the -// behavior's own, overridden by the blocks entered around it, innermost last. -func (env *stmtEnv) values() map[string]Value { +// values is the values a statement reads, with locals overriding behavior data. +func (env *stmtEnv) values(ctx *Context) (map[string]Value, error) { merged := make(map[string]Value, env.data.width()) for _, f := range env.enclosing { - f.each(func(name string, value Value) { merged[name] = value }) + if err := f.eachCurrent(ctx, func(name string, value Value) { merged[name] = value }); err != nil { + return nil, err + } + } + if err := env.data.eachCurrent(ctx, func(name string, value Value) { merged[name] = value }); err != nil { + return nil, err } - env.data.each(func(name string, value Value) { merged[name] = value }) - for _, frame := range env.frames { + if err := ctx.deriveBodyCells(env.localCells); err != nil { + return nil, err + } + maps.Copy(merged, env.locals) + for i, frame := range env.frames { + if err := ctx.deriveBodyCells(env.cells[i]); err != nil { + return nil, err + } maps.Copy(merged, frame) } - return merged + return merged, nil +} + +func (env *stmtEnv) constraintResult(ctx *Context) (frame, error) { + values := make(map[string]Value, env.data.width()+len(env.locals)) + if err := env.data.eachCurrent(ctx, func(name string, value Value) { values[name] = value }); err != nil { + return frame{}, err + } + if err := ctx.deriveBodyCells(env.localCells); err != nil { + return frame{}, err + } + maps.Copy(values, env.locals) + result := env.data + result.unvalued = maps.Clone(env.data.unvalued) + for name := range env.locals { + delete(result.unvalued, name) + } + for name := range env.unvaluedLocal { + result.markUnvalued(name) + } + var cells *bodyCells + if env.localCells != nil { + localCells := *env.localCells + localCells.vars = values + localCells.cells = maps.Clone(env.localCells.cells) + localCells.order = slices.Clone(env.localCells.order) + cells = &localCells + } + result.vars, result.cells, result.ensureCells = values, cells, nil + return result, nil +} + +// localFrames returns the entered block maps and their dependency-cell stores. +func (env *stmtEnv) localFrames() ([]map[string]Value, []*bodyCells) { + locals := make([]map[string]Value, 0, len(env.frames)+1) + cells := make([]*bodyCells, 0, len(env.frames)+1) + locals = append(locals, env.locals) + cells = append(cells, env.localCells) + locals = append(locals, env.frames...) + cells = append(cells, env.cells...) + return locals, cells +} + +// localFrame exposes the behavior's root locals as an evaluation frame. +func (env *stmtEnv) localFrame() frame { + local := env.bodyFrame(env.locals, env.localCells, env.unvaluedLocal) + return local +} + +// bodyFrame marks local values as lexical bindings in the run that owns the data frame. +func (env *stmtEnv) bodyFrame(vars map[string]Value, cells *bodyCells, unvalued map[string]bool) frame { + local := mapFrame(vars) + local.owner, local.perf = env.data.owner, env.data.perf + local.run, local.performed = env.data.run, env.data.performed + local.merged, local.firing = env.data.merged, env.data.firing + local.cells = cells + local.lexical = true + local.unvalued = unvalued + return local } // assign writes to the innermost entered block that declares name, or to the // behavior's data when that holds the name, and reports whether it found one. // A name neither declares is the host's to decide on. -func (env *stmtEnv) assign(name string, value Value) bool { - if env.assignLocal(name, value) { +func (env *stmtEnv) assign(ctx *Context, name string, value Value) bool { + if env.assignLocal(ctx, name, value) { return true } if env.data.has(name) { - env.data.set(name, value) + env.data.setBody(ctx, name, value) return true } return false @@ -141,6 +300,16 @@ const ( flowReturn ) +// bodyBindingMirror lets a statement host mirror tracked local bindings. +type bodyBindingMirror interface { + mirrorBodyBinding(name string, cell *bodyCell, value *Value) error +} + +// rootLocalMirror lets a statement host mirror root-local values. +type rootLocalMirror interface { + mirrorRootLocal(name string, value Value) error +} + // stmtHost is the behavior a statement engine runs statements for: it names // itself in diagnostics and decides the statements only it can state — sends, // returns, assignments reaching outside the body, effects. @@ -222,11 +391,11 @@ type stmtEngine struct { // newStmtEngineOver returns an engine running statements against data, which also // read outer value maps — the attributes of the enclosing states — innermost last. -func newStmtEngineOver(ctx *Context, host stmtHost, data frame, outer []map[string]Value) *stmtEngine { +func newStmtEngineOver(ctx *Context, host stmtHost, data frame, outer []frame) *stmtEngine { return &stmtEngine{ ctx: ctx, host: host, - env: &stmtEnv{data: data, outer: outer}, + env: &stmtEnv{data: data, outer: outer, locals: make(map[string]Value)}, activation: ctx.newActivation(), thisOccurrence: host.materializeOccurrence, } @@ -238,7 +407,7 @@ func newStmtEngineIn(ctx *Context, host stmtHost, data frame, enclosing []frame) return &stmtEngine{ ctx: ctx, host: host, - env: &stmtEnv{data: data, enclosing: enclosing}, + env: &stmtEnv{data: data, enclosing: enclosing, locals: make(map[string]Value)}, activation: ctx.newActivation(), thisOccurrence: host.materializeOccurrence, } @@ -247,23 +416,34 @@ func newStmtEngineIn(ctx *Context, host stmtHost, data frame, enclosing []frame) // finish ends the activation the engine's statements ran in, discarding what the // calc usages read in them computed. func (e *stmtEngine) finish() { + e.ctx.forgetBodyCells(e.env.localCells) e.ctx.endActivation(e.activation) } +// complete freezes the behavior's remaining bindings and ends its activation. +func (e *stmtEngine) complete() error { + err := e.ctx.freezeBodyCells(e.env.localCells) + e.ctx.endActivation(e.activation) + return err +} + // evalIn returns an evaluation context resolving names in the scope the // statement was written in, reading the behavior's data and the frames entered, // innermost last so a block-local name shadows an outer one. func (e *stmtEngine) evalIn(scope *symbols.Scope) *EvalContext { frames := append(e.frameBuf[:0], e.env.enclosing...) frames = append(frames, e.env.data) - for _, outer := range e.env.outer { - frames = append(frames, mapFrame(outer)) - } + frames = append(frames, e.env.outer...) if e.env.perf != nil { frames = append(frames, performanceFrame(e.env.perf)) } - for _, local := range e.env.frames { - frames = append(frames, mapFrame(local)) + root := e.env.bodyFrame(e.env.locals, e.env.localCells, e.env.unvaluedLocal) + root.ensureCells = func() *bodyCells { return e.ensureRootCells() } + frames = append(frames, root) + for i := range e.env.frames { + frames = append(frames, e.env.bodyFrame(e.env.frames[i], e.env.cells[i], e.env.unvalued[i])) + index := i + frames[len(frames)-1].ensureCells = func() *bodyCells { return e.ensureLocalCells(index) } } e.frameBuf = frames ec := &e.scratch @@ -282,6 +462,33 @@ func (e *stmtEngine) evalIn(scope *symbols.Scope) *EvalContext { return ec } +// evalInDepth resolves a declaration with only the lexical frames visible at depth. +func (e *stmtEngine) evalInDepth(scope *symbols.Scope, depth int) *EvalContext { + frames := make([]frame, 0, len(e.env.enclosing)+1+len(e.env.outer)+depth+1) + frames = append(frames, e.env.enclosing...) + frames = append(frames, e.env.data) + frames = append(frames, e.env.outer...) + if e.env.perf != nil { + frames = append(frames, performanceFrame(e.env.perf)) + } + root := e.env.bodyFrame(e.env.locals, e.env.localCells, e.env.unvaluedLocal) + root.ensureCells = func() *bodyCells { return e.ensureRootCells() } + frames = append(frames, root) + if depth > len(e.env.frames) { + depth = len(e.env.frames) + } + for i, local := range e.env.frames[:depth] { + frames = append(frames, e.env.bodyFrame(local, e.env.cells[i], e.env.unvalued[i])) + index := i + frames[len(frames)-1].ensureCells = func() *bodyCells { return e.ensureLocalCells(index) } + } + return &EvalContext{ + ctx: e.ctx, scope: scope, self: e.host.performer(), + occurrence: e.host.occurrence(), thisOccurrence: e.thisOccurrence, + frames: frames, trace: e.ctx.trace, inBehaviorBody: true, activation: e.activation, + } +} + // engineFrame is the engine of a body that paused, kept with the values and // activation it runs in until the body is resumed and ends. type engineFrame struct{ engine *stmtEngine } @@ -290,7 +497,8 @@ func (f *engineFrame) abandon(*Context) { f.engine.finish() } func (f *engineFrame) clone() bodyFrame { engine, env := *f.engine, *f.engine.env - env.frames, env.unvalued = slices.Clone(env.frames), slices.Clone(env.unvalued) + env.frames, env.cells, env.unvalued = slices.Clone(env.frames), slices.Clone(env.cells), slices.Clone(env.unvalued) + env.unvaluedLocal = maps.Clone(env.unvaluedLocal) engine.env, engine.scratch, engine.frameBuf = &env, EvalContext{}, nil return &engineFrame{engine: &engine} } @@ -311,8 +519,11 @@ func (ctx *Context) runStatements(build func() *stmtEngine, stmts []lower.Statem ctx.pushPaused(f) return flow, err } - f.engine.finish() - return flow, err + if err != nil { + f.engine.finish() + return flow, err + } + return flow, f.engine.complete() } // stmtListFrame is where a statement list paused: at its i-th statement, which @@ -643,6 +854,9 @@ func (e *stmtEngine) execute(stmt lower.Statement) (stmtFlow, error) { // The qualifier denoting this body's own run makes the write the // unqualified one: the host writes and streams it as `assign n := 3`. if e.env.data.runs(ec.ctx, s.Owner) { + if !e.host.declaredOutput(s.Target) && e.env.rootDeclares(s.Target) { + return flowNext, e.host.assignOuter(e.env, s.Target, value, s) + } if !e.host.declaredOutput(s.Target) && e.env.data.has(s.Target) { return flowNext, e.host.assignData(e.env, s.Target, value, s) } @@ -656,7 +870,13 @@ func (e *stmtEngine) execute(stmt lower.Statement) (stmtFlow, error) { if err := e.ctx.checkBodyWrite(e.host, s, &value); err != nil { return flowNext, err } - e.env.assignLocal(s.Target, value) + rootLocal := e.env.rootLocal(s.Target) + e.env.assignLocal(e.ctx, s.Target, value) + if rootLocal { + if mirror, ok := e.host.(rootLocalMirror); ok { + return flowNext, mirror.mirrorRootLocal(s.Target, value) + } + } return flowNext, nil } if !e.host.declaredOutput(s.Target) { @@ -666,6 +886,73 @@ func (e *stmtEngine) execute(stmt lower.Statement) (stmtFlow, error) { } return flowNext, e.host.assignOuter(e.env, s.Target, value, s) case lower.Declare: + if s.Binding && s.Value != nil { + var cells *bodyCells + name := s.Name + if depth := len(e.env.frames); depth > 0 { + cells = e.ensureLocalCells(depth - 1) + } else { + cells = e.ensureRootCells() + } + check := func(value *Value) error { + return e.ctx.checkBodyDeclaration(s.Scope, e.host.describe(), name, value) + } + bindingContext := e.evalIn(s.Scope) + visible := make([]map[string]bool, len(bindingContext.frames)) + for i, frame := range bindingContext.frames { + visible[i] = frame.visibleNames() + } + var cell *bodyCell + var onDerived func(*Value) error + if len(e.env.frames) == 0 { + if mirror, ok := e.host.(bodyBindingMirror); ok { + onDerived = func(value *Value) error { + return mirror.mirrorBodyBinding(name, cell, value) + } + } + } + cell = e.ctx.registerBodyBinding(cells, name, s.Value, s.Scope, check, onDerived) + cell.binding.visible = visible + cell.binding.limitFrames = true + if _, err := e.ctx.deriveBodyCell(cells, name, cell); err != nil { + var checkErr bodyBindingCheckError + if errors.As(err, &checkErr) { + return flowNext, checkErr.err + } + return flowNext, fmt.Errorf("eval declaration %s: %w", s.Name, err) + } + return flowNext, nil + } + if s.BodyData { + value := Value{Kind: ValNull} + if s.Value != nil { + evaluated, err := e.evalIn(s.Scope).Eval(s.Value) + if err != nil { + return flowNext, fmt.Errorf("eval declaration %s: %w", s.Name, err) + } + if err := e.ctx.checkBodyDeclaration(s.Scope, e.host.describe(), s.Name, &evaluated); err != nil { + return flowNext, err + } + value = evaluated + } + e.env.data.setBody(e.ctx, s.Name, value) + return flowNext, nil + } + if e.host.declaredOutput(s.Name) { + value := Value{Kind: ValNull} + if s.Value != nil { + evaluated, err := e.evalIn(s.Scope).Eval(s.Value) + if err != nil { + return flowNext, fmt.Errorf("eval declaration %s: %w", s.Name, err) + } + if err := e.ctx.checkBodyDeclaration(s.Scope, e.host.describe(), s.Name, &evaluated); err != nil { + return flowNext, err + } + value = evaluated + } + e.env.data.setBody(e.ctx, s.Name, value) + return flowNext, nil + } if s.Value != nil { evaluated, err := e.evalIn(s.Scope).Eval(s.Value) if err != nil { @@ -674,7 +961,7 @@ 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 } - e.env.declare(s.Name, evaluated) + e.env.declare(e.ctx, s.Name, evaluated) return flowNext, nil } // A constraint body's performance declares a valueless name as missing @@ -683,7 +970,7 @@ func (e *stmtEngine) execute(stmt lower.Statement) (stmtFlow, error) { if _, constraint := e.host.(*constraintStmtHost); constraint { e.env.declareUnvalued(s.Name) } else { - e.env.declare(s.Name, Value{Kind: ValNull}) + e.env.declare(e.ctx, s.Name, Value{Kind: ValNull}) } return flowNext, nil case lower.DeclareUsage: @@ -744,6 +1031,7 @@ func (e *stmtEngine) assert(s lower.Assert) error { // this execution of the body reads starts here, so an evaluation of the same // usage from before the declaration was reached is discarded. func (e *stmtEngine) declareUsage(stmt lower.DeclareUsage) error { + // Known limitation: the `=` pins of a body-local calc usage are evaluated once per declaration and do not track later writes. sym, err := e.ctx.bodyUsageSymbol(stmt) if err != nil { return fmt.Errorf("%s: %w", e.host.describe(), err) @@ -791,33 +1079,44 @@ func (e *stmtEngine) ifStatement(stmt lower.If) (stmtFlow, error) { // its activation, and the one around it. type blockFrame struct { locals map[string]Value + cells *bodyCells unvalued map[string]bool activation int64 outer int64 } -func (f *blockFrame) abandon(ctx *Context) { ctx.endActivation(f.activation) } +// abandon releases dependencies and activation state for a discarded block. +func (f *blockFrame) abandon(ctx *Context) { + ctx.forgetBodyCells(f.cells) + ctx.endActivation(f.activation) +} func (f *blockFrame) clone() bodyFrame { c := *f - c.locals, c.unvalued = maps.Clone(f.locals), maps.Clone(f.unvalued) + if f.cells == nil { + c.locals = maps.Clone(f.locals) + } + c.unvalued = maps.Clone(f.unvalued) return &c } // enterBlock enters a frame and an activation for a block about to run, or the // ones a paused block ran in; leave restores what was around them. -func (e *stmtEngine) enterBlock(f *blockFrame) (leave func()) { +func (e *stmtEngine) enterBlock(f *blockFrame) (leave func(bool)) { if f.locals == nil { - f.locals = e.env.enter() + f.locals = e.env.enter(e) + f.cells = e.env.cells[len(e.env.cells)-1] f.activation, f.outer = e.ctx.newActivation(), e.activation } else { e.env.frames = append(e.env.frames, f.locals) + e.env.cells = append(e.env.cells, f.cells) e.env.unvalued = append(e.env.unvalued, f.unvalued) } e.activation = f.activation - return func() { + return func(forget bool) { f.unvalued = e.env.unvalued[len(e.env.unvalued)-1] - e.env.leave() + f.cells = e.env.cells[len(e.env.cells)-1] + e.env.leave(e.ctx, forget) e.activation = f.outer } } @@ -834,11 +1133,12 @@ func (e *stmtEngine) block(block lower.Block) (stmtFlow, error) { } leave := e.enterBlock(f) flow, err := e.runBlock(block) - leave() if paused(err) { + leave(false) e.ctx.pushPaused(f) return flow, err } + leave(true) e.ctx.endActivation(f.activation) return flow, err } @@ -989,6 +1289,7 @@ func (e *stmtEngine) enterActivation() func() { // iteration under way with its activation, and the activation around the loop. type loopFrame struct { locals map[string]Value + cells *bodyCells unvalued map[string]bool iteration int activation int64 @@ -997,27 +1298,44 @@ type loopFrame struct { elements []Value } -func (f *loopFrame) abandon(ctx *Context) { ctx.endActivation(f.activation) } +// abandon releases dependencies and activation state for a discarded loop. +func (f *loopFrame) abandon(ctx *Context) { + ctx.forgetBodyCells(f.cells) + ctx.endActivation(f.activation) +} func (f *loopFrame) clone() bodyFrame { c := *f - c.locals, c.unvalued = maps.Clone(f.locals), maps.Clone(f.unvalued) + if f.cells == nil { + c.locals = maps.Clone(f.locals) + } + c.unvalued = maps.Clone(f.unvalued) return &c } +// syncLoopCells keeps the loop frame's cell store aligned with its local frame. +func (e *stmtEngine) syncLoopCells(f *loopFrame) { + if depth := len(e.env.cells); depth > 0 { + f.cells = e.env.cells[depth-1] + } +} + // enterLoop enters the frame the loop's body declares into, or re-enters the one a // paused loop ran in; leave restores what was around it. -func (e *stmtEngine) enterLoop(f *loopFrame) (leave func()) { +func (e *stmtEngine) enterLoop(f *loopFrame) (leave func(bool)) { if f.locals == nil { - f.locals = e.env.enter() + f.locals = e.env.enter(e) + f.cells = e.env.cells[len(e.env.cells)-1] f.outer = e.activation } else { e.env.frames = append(e.env.frames, f.locals) + e.env.cells = append(e.env.cells, f.cells) e.env.unvalued = append(e.env.unvalued, f.unvalued) } - return func() { + return func(forget bool) { f.unvalued = e.env.unvalued[len(e.env.unvalued)-1] - e.env.leave() + f.cells = e.env.cells[len(e.env.cells)-1] + e.env.leave(e.ctx, forget) e.activation = f.outer } } @@ -1065,10 +1383,12 @@ func (e *stmtEngine) loop(stmt lower.Loop) (stmtFlow, error) { } resumed = resumed && !e.ctx.yieldedHere() leave := e.enterLoop(f) - defer leave() + preserve := false + defer func() { leave(!preserve) }() for { if err := e.yieldBody(); err != nil { + preserve = true return flowNext, e.ctx.pausing(f, err) } if !resumed { @@ -1079,6 +1399,7 @@ func (e *stmtEngine) loop(stmt lower.Loop) (stmtFlow, error) { flow, done, err := e.iteration(stmt, f, resumed) resumed = false if err != nil || done || flow == flowReturn { + preserve = paused(err) return flow, e.ctx.pausing(f, err) } e.bodyPerformed() @@ -1102,8 +1423,11 @@ func (e *stmtEngine) iteration(stmt lower.Loop, f *loopFrame, resumed bool) (flo } } clear(f.locals) + e.syncLoopCells(f) + e.ctx.resetBodyCells(f.cells) } flow, err = e.runBlock(stmt.Body) + e.syncLoopCells(f) if err != nil || flow == flowReturn { return flow, true, err } @@ -1152,10 +1476,12 @@ func (e *stmtEngine) forLoop(stmt lower.Loop) (stmtFlow, error) { } resumed = resumed && !e.ctx.yieldedHere() leave := e.enterLoop(f) - defer leave() + preserve := false + defer func() { leave(!preserve) }() for f.iteration < len(f.elements) || resumed { if err := e.yieldBody(); err != nil { + preserve = true return flowNext, e.ctx.pausing(f, err) } if !resumed { @@ -1166,6 +1492,7 @@ func (e *stmtEngine) forLoop(stmt lower.Loop) (stmtFlow, error) { flow, err := e.forIteration(stmt, f, resumed) resumed = false if err != nil || flow == flowReturn { + preserve = paused(err) return flow, e.ctx.pausing(f, err) } e.bodyPerformed() @@ -1180,9 +1507,13 @@ func (e *stmtEngine) forIteration(stmt lower.Loop, f *loopFrame, resumed bool) ( defer func() { e.endIteration(f, err) }() if !resumed { clear(f.locals) + e.syncLoopCells(f) + e.ctx.resetBodyCells(f.cells) f.locals[stmt.Variable] = f.elements[f.iteration-1] } - return e.runBlock(stmt.Body) + flow, err = e.runBlock(stmt.Body) + e.syncLoopCells(f) + return flow, err } // stmtLabel names a statement for a trace by what it does and, where it has diff --git a/internal/exec/runtime/testdata/conformance/README.md b/internal/exec/runtime/testdata/conformance/README.md index 370a3d5b47..63640e62e8 100644 --- a/internal/exec/runtime/testdata/conformance/README.md +++ b/internal/exec/runtime/testdata/conformance/README.md @@ -128,7 +128,7 @@ one: } ``` -- `outcomes`: at least two complete results. Each entry carries the `outputs` of +- `outcomes`: one or more complete results. Each entry carries the `outputs` of an action case, or the `finalState` / `stateVisits` / `outputs` of a state case, with the meaning those keys have above, plus an optional `probability`: the exact model-draw probability when every matched explored outcome has an @@ -177,15 +177,18 @@ a case that hits it fails with a message telling the author to raise it: } ``` -- `exploreBudget`: optional beside `outcomes`; each of `runs` and `depth` defaults - to the default budget's when omitted. A budget without `outcomes`, a `runs` - below 1 or a `depth` below 0 is a schema error the test reports. Raising a - budget is the answer to a model with more linearizations than the default - covers, never to an outcome the set is missing: an unlisted outcome is a - derivation to add to the oracle or a bug to fix. - -Cases without `outcomes` are not explored by the harness. The default schedule is -deterministic, so a case with an admissible set still keeps its exact golden trace. +- `exploreBudget`: optional for an action or state case with a single result or + `outcomes`; each of `runs` and `depth` defaults to the default budget's when + omitted. A budget without a result to check, a `runs` below 1 or a `depth` + below 0 is a schema error the test reports. Raising a budget is the answer to + a model with more linearizations than the default covers, never to an outcome + the set is missing: an unlisted outcome is a derivation to add to the oracle + or a bug to fix. + +`TestExecutionConformance` explores cases with `outcomes`; the check corpus can +also explore a single-result case with a `.check.expected.json` entry. The +default schedule is deterministic, so a case with an admissible set still keeps +its exact golden trace. ### Scheduling Policy diff --git a/internal/exec/runtime/testdata/conformance/action_block_body_feature_bindings.expected.json b/internal/exec/runtime/testdata/conformance/action_block_body_feature_bindings.expected.json new file mode 100644 index 0000000000..39cf0cb8fe --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_block_body_feature_bindings.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "evaluate": "test::run", + "outputs": { + "source": {"type": "Integer", "value": 8}, + "total": {"type": "Integer", "value": 30} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_block_body_feature_bindings.sysml b/internal/exec/runtime/testdata/conformance/action_block_body_feature_bindings.sysml new file mode 100644 index 0000000000..fccd6affe5 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_block_body_feature_bindings.sysml @@ -0,0 +1,28 @@ +package test { + private import ScalarValues::*; + + action run { + attribute source : Integer := 0; + out attribute total : Integer := 0; + + first start; + then action blocks { + while source < 3 { + attribute local : Integer = source; + assign source := source + 1; + then assign total := total + local; + } + then if source == 3 { + attribute branch : Integer = source; + assign source := 6; + then assign total := total + branch; + } + then for i in 1..2 { + attribute each : Integer = source + i; + assign source := source + 1; + then assign total := total + each; + } + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_binding.expected.json b/internal/exec/runtime/testdata/conformance/action_body_feature_binding.expected.json new file mode 100644 index 0000000000..24156042a3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_binding.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "evaluate": "test::act", + "trace": true, + "outputs": { + "a": {"type": "Integer", "value": 5}, + "s": {"type": "Integer", "value": 10}, + "r": {"type": "Integer", "value": 10} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_binding.sysml b/internal/exec/runtime/testdata/conformance/action_body_feature_binding.sysml new file mode 100644 index 0000000000..119e9cb668 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_binding.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + + action act { + attribute a : Integer := 0; + attribute s : Integer = a * 2; + out attribute r : Integer; + + first start; + then action write { assign a := 5; } + then action read { assign r := s; } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_binding.trace.golden b/internal/exec/runtime/testdata/conformance/action_body_feature_binding.trace.golden new file mode 100644 index 0000000000..3e668ea1f7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_binding.trace.golden @@ -0,0 +1,11 @@ +step 1: token 1@write +stmt assign a + eval literal 5 -> 5 +step 2: token 1@read +stmt assign r + eval feature a -> 5 + eval literal 2 -> 2 + eval operator * -> 10 + eval feature s -> 10 +step 3: token 1@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_initial_binding.expected.json b/internal/exec/runtime/testdata/conformance/action_body_feature_initial_binding.expected.json new file mode 100644 index 0000000000..38ae5248f9 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_initial_binding.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "evaluate": "test::run", + "outputs": { + "source": {"type": "Integer", "value": 5}, + "snapshot": {"type": "Integer", "value": 2}, + "fallback": {"type": "Integer", "value": 9}, + "result": {"type": "Integer", "value": 29} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_initial_binding.sysml b/internal/exec/runtime/testdata/conformance/action_body_feature_initial_binding.sysml new file mode 100644 index 0000000000..9a68e00da7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_initial_binding.sysml @@ -0,0 +1,18 @@ +package test { + private import ScalarValues::*; + + action run { + attribute source : Integer := 2; + attribute snapshot : Integer := source; + attribute fallback : Integer default source; + out attribute result : Integer; + + first start; + then action change { + assign source := 5; + then assign fallback := 9; + then assign result := snapshot * 10 + fallback; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_occurrence_mirror.expected.json b/internal/exec/runtime/testdata/conformance/action_body_feature_occurrence_mirror.expected.json new file mode 100644 index 0000000000..b4e9ed7916 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_occurrence_mirror.expected.json @@ -0,0 +1,9 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::Host", + "slots": { + "mirrored": {"type": "Integer", "value": 10}, + "result": {"type": "Integer", "value": 10} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_occurrence_mirror.sysml b/internal/exec/runtime/testdata/conformance/action_body_feature_occurrence_mirror.sysml new file mode 100644 index 0000000000..47c1bb5219 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_occurrence_mirror.sysml @@ -0,0 +1,25 @@ +package test { + private import ScalarValues::*; + + part def Host { + attribute mirrored : Integer := 0; + attribute result : Integer := 0; + + action def Bind { + in ref context : Host[1]; + attribute source : Integer := 0; + attribute live : Integer = source * 2; + + first start; + then action observe { assign context.mirrored := this.live; } + then action write { assign source := 5; } + then action read { + assign context.mirrored := this.live; + assign context.result := live; + } + then done; + } + + perform action bind : Bind { in ref :>> context = this; } + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_pin_write.expected.json b/internal/exec/runtime/testdata/conformance/action_body_feature_pin_write.expected.json new file mode 100644 index 0000000000..894dd478b9 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_pin_write.expected.json @@ -0,0 +1,10 @@ +{ + "type": "action", + "evaluate": "test::run", + "outputs": { + "source": {"type": "Integer", "value": 8}, + "result": {"type": "Integer", "value": 4}, + "consume.value": {"type": "Integer", "value": 4}, + "consume.result": {"type": "Integer", "value": 4} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_pin_write.sysml b/internal/exec/runtime/testdata/conformance/action_body_feature_pin_write.sysml new file mode 100644 index 0000000000..191b0ff06c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_pin_write.sysml @@ -0,0 +1,21 @@ +package test { + private import ScalarValues::*; + + action def Consumer { + in value : Integer = 1; + out result : Integer; + assign result := value; + } + + action run { + attribute source : Integer := 4; + out attribute result : Integer; + first start; + then action consume : Consumer { in value = source; } + then action update { + assign source := 8; + assign result := consume.result; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_read_only.expected.json b/internal/exec/runtime/testdata/conformance/action_body_feature_read_only.expected.json new file mode 100644 index 0000000000..9869622047 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_read_only.expected.json @@ -0,0 +1,5 @@ +{ + "type": "action", + "evaluate": "test::run", + "error": "feature is read-only: locked is constant" +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_read_only.sysml b/internal/exec/runtime/testdata/conformance/action_body_feature_read_only.sysml new file mode 100644 index 0000000000..ab27f36cec --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_read_only.sysml @@ -0,0 +1,9 @@ +package test { + private import ScalarValues::*; + + action run { + constant attribute locked : Integer = 1; + first start; + then action write { assign locked := 2; } + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_written.expected.json b/internal/exec/runtime/testdata/conformance/action_body_feature_written.expected.json new file mode 100644 index 0000000000..460c196e52 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_written.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "evaluate": "test::run", + "outputs": { + "source": {"type": "Integer", "value": 5}, + "live": {"type": "Integer", "value": 99}, + "result": {"type": "Integer", "value": 99} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_body_feature_written.sysml b/internal/exec/runtime/testdata/conformance/action_body_feature_written.sysml new file mode 100644 index 0000000000..66dd4ae21d --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_body_feature_written.sysml @@ -0,0 +1,17 @@ +package test { + private import ScalarValues::*; + + action run { + attribute source : Integer := 1; + attribute live : Integer = source * 2; + out attribute result : Integer; + + first start; + then action update { + assign live := 99; + then assign source := 5; + then assign result := live; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_inherited_default_root.expected.json b/internal/exec/runtime/testdata/conformance/action_inherited_default_root.expected.json index 6a05291470..9dd5b3b548 100644 --- a/internal/exec/runtime/testdata/conformance/action_inherited_default_root.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_inherited_default_root.expected.json @@ -3,8 +3,8 @@ "evaluate": "test::Derived", "outputs": { "x": {"type": "Integer", "value": 10}, - "z": {"type": "Integer", "value": 6}, + "z": {"type": "Integer", "value": 20}, "y": {"type": "Integer", "value": 4}, - "r": {"type": "Integer", "value": 6} + "r": {"type": "Integer", "value": 20} } } diff --git a/internal/exec/runtime/testdata/conformance/action_inherited_default_root.sysml b/internal/exec/runtime/testdata/conformance/action_inherited_default_root.sysml index 9f95bcea42..55f1f2a830 100644 --- a/internal/exec/runtime/testdata/conformance/action_inherited_default_root.sysml +++ b/internal/exec/runtime/testdata/conformance/action_inherited_default_root.sysml @@ -1,9 +1,8 @@ package test { private import ScalarValues::*; - // The root performance seeds the defaults inherited from the general action - // when it starts, as it seeds its own: z holds x * 2 as x was then, not as - // the body later reassigns it. + // The unsupplied `in z = x * 2` parameter binds z to the expression for the + // performance, so it follows the value assigned to x later in the body. action def Base { in x : Integer = 3; in z : Integer = x * 2; diff --git a/internal/exec/runtime/testdata/conformance/action_inherited_default_root_fallback.expected.json b/internal/exec/runtime/testdata/conformance/action_inherited_default_root_fallback.expected.json new file mode 100644 index 0000000000..b5503500d2 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_inherited_default_root_fallback.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "evaluate": "test::Derived", + "outputs": { + "x": {"type": "Integer", "value": 10}, + "z": {"type": "Integer", "value": 6}, + "r": {"type": "Integer", "value": 6} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_inherited_default_root_fallback.sysml b/internal/exec/runtime/testdata/conformance/action_inherited_default_root_fallback.sysml new file mode 100644 index 0000000000..8e39bf1d06 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_inherited_default_root_fallback.sysml @@ -0,0 +1,17 @@ +package test { + private import ScalarValues::*; + + // `default` supplies z only when no value is passed; the value does not track x. + action def Base { + in x : Integer = 3; + in z : Integer default x * 2; + out r : Integer; + } + + action def Derived :> Base { + first start; + then assign x := 10; + then assign r := z; + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_inherited_supplied_parameter.expected.json b/internal/exec/runtime/testdata/conformance/action_inherited_supplied_parameter.expected.json new file mode 100644 index 0000000000..a6b2e11799 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_inherited_supplied_parameter.expected.json @@ -0,0 +1,7 @@ +{ + "type": "action", + "evaluate": "test::caller", + "outputs": { + "result": {"type": "Integer", "value": 7} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_inherited_supplied_parameter.sysml b/internal/exec/runtime/testdata/conformance/action_inherited_supplied_parameter.sysml new file mode 100644 index 0000000000..d0ac57626c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_inherited_supplied_parameter.sysml @@ -0,0 +1,25 @@ +package test { + private import ScalarValues::*; + + action def Base { + in x : Integer = 3; + in z : Integer = x * 2; + out r : Integer; + + first start; + then assign x := 10; + then assign r := z; + then done; + } + + action caller { + out attribute result : Integer; + + first start; + then action run = Base(x = 3, z = 7); + then action keep { + assign result := run.r; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_nested_body_feature_binding.expected.json b/internal/exec/runtime/testdata/conformance/action_nested_body_feature_binding.expected.json new file mode 100644 index 0000000000..15fbb643fa --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_nested_body_feature_binding.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "evaluate": "test::outer", + "outputs": { + "result": {"type": "Integer", "value": 12}, + "child.x": {"type": "Integer", "value": 12}, + "child.y": {"type": "Integer", "value": 12} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_nested_body_feature_binding.sysml b/internal/exec/runtime/testdata/conformance/action_nested_body_feature_binding.sysml new file mode 100644 index 0000000000..2ae65fcb97 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_nested_body_feature_binding.sysml @@ -0,0 +1,21 @@ +package test { + private import ScalarValues::*; + + action outer { + attribute a : Integer := 2; + out attribute result : Integer; + + first start; + then action child { + attribute x : Integer = a * 3; + out attribute y : Integer; + + first start; + then action change { assign a := 4; } + then action read { assign y := x; } + then done; + } + then action collect { assign result := child.y; } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_qualified_member_under_shadowing_local.expected.json b/internal/exec/runtime/testdata/conformance/action_qualified_member_under_shadowing_local.expected.json new file mode 100644 index 0000000000..a56e93f975 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_qualified_member_under_shadowing_local.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "evaluate": "test::Act", + "outputs": { + "a": {"type": "Integer", "value": 4}, + "r": {"type": "Integer", "value": 1}, + "calcResult": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_qualified_member_under_shadowing_local.sysml b/internal/exec/runtime/testdata/conformance/action_qualified_member_under_shadowing_local.sysml new file mode 100644 index 0000000000..dac7f998d3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_qualified_member_under_shadowing_local.sysml @@ -0,0 +1,23 @@ +package test { + private import ScalarValues::*; + + calc def C { + attribute a : Integer := 1; + calc inner { + attribute a : Integer := 9; + C::a + } + } + + action def Act { + attribute a : Integer := 1; + out attribute r : Integer; + out attribute calcResult : Integer; + if true { + attribute a : Integer := 9; + assign r := Act::a; + assign Act::a := 4; + } + assign calcResult := C::inner(); + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.check.expected.json b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.check.expected.json index d75f916d89..34adf1121d 100644 --- a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.check.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.check.expected.json @@ -1,5 +1,5 @@ { - "verdict": "divergent", - "divergent": {"c": ["1", "2", "3"]}, - "agreed": {} + "verdict": "no violation, exhaustive", + "divergent": {}, + "agreed": {"c": "3"} } diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.expected.json b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.expected.json index e241fb4ccc..a1c7149f79 100644 --- a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.expected.json @@ -2,10 +2,5 @@ "type": "action", "exploreBudget": {"runs": 65536}, "libraries": true, - "outcomes": [ - {"outputs": {"c": {"type": "Integer", "value": 1}}}, - {"outputs": {"c": {"type": "Integer", "value": 2}}}, - {"outputs": {"c": {"type": "Integer", "value": 3}}} - ], - "admissible": "Repeated action steps and shared writes" + "outputs": {"c": {"type": "Integer", "value": 3}} } diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.sysml b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.sysml index 73a91a0a09..0a04ec5338 100644 --- a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.sysml +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers.sysml @@ -4,8 +4,8 @@ package test { action def SharedWriters { attribute c : Integer = 0; first start then a; - // Each performance snapshots shared c in its own l; the final value depends on - // how many snapshots happen before the last write. + // Each performance binds l to the current c; the repeated writes therefore + // advance the shared value to 3 under every interleaving. action a[3] { attribute l : Integer = c; first start then w; diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.check.expected.json b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.check.expected.json new file mode 100644 index 0000000000..d75f916d89 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.check.expected.json @@ -0,0 +1,5 @@ +{ + "verdict": "divergent", + "divergent": {"c": ["1", "2", "3"]}, + "agreed": {} +} diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.expected.json b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.expected.json new file mode 100644 index 0000000000..e241fb4ccc --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.expected.json @@ -0,0 +1,11 @@ +{ + "type": "action", + "exploreBudget": {"runs": 65536}, + "libraries": true, + "outcomes": [ + {"outputs": {"c": {"type": "Integer", "value": 1}}}, + {"outputs": {"c": {"type": "Integer", "value": 2}}}, + {"outputs": {"c": {"type": "Integer", "value": 3}}} + ], + "admissible": "Repeated action steps and shared writes" +} diff --git a/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.sysml b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.sysml new file mode 100644 index 0000000000..1304022c51 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_step_multiplicity_shared_writers_initial.sysml @@ -0,0 +1,17 @@ +package test { + private import ScalarValues::*; + + action def SharedWriters { + attribute c : Integer = 0; + first start then a; + // Each performance snapshots shared c in its own l; the final value depends on + // how many snapshots happen before the last write. + action a[3] { + attribute l : Integer := c; + first start then w; + action w { assign c := l + 1; } + then done; + } + then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_body_feature_binding.expected.json b/internal/exec/runtime/testdata/conformance/calc_body_feature_binding.expected.json new file mode 100644 index 0000000000..5b8846a56b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_body_feature_binding.expected.json @@ -0,0 +1,6 @@ +{ + "type": "calcUsage", + "evaluate": "test::c", + "outputs": {}, + "result": {"type": "Integer", "value": 12} +} diff --git a/internal/exec/runtime/testdata/conformance/calc_body_feature_binding.sysml b/internal/exec/runtime/testdata/conformance/calc_body_feature_binding.sysml new file mode 100644 index 0000000000..2d7f2d1a7a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/calc_body_feature_binding.sysml @@ -0,0 +1,13 @@ +package test { + private import ScalarValues::*; + + calc def Recompute { + in initial : Integer; + attribute current : Integer := initial; + attribute doubled : Integer = current * 2; + assign current := 6; + return : Integer = doubled; + } + + calc c : Recompute { in initial = 3; } +} diff --git a/internal/exec/runtime/testdata/conformance/calc_usage_outputs_one_binding.trace.golden b/internal/exec/runtime/testdata/conformance/calc_usage_outputs_one_binding.trace.golden index 7b8d228dcb..70c8552638 100644 --- a/internal/exec/runtime/testdata/conformance/calc_usage_outputs_one_binding.trace.golden +++ b/internal/exec/runtime/testdata/conformance/calc_usage_outputs_one_binding.trace.golden @@ -20,6 +20,8 @@ enter calc test::Interleaved output test::Interleaved::p.b = 10.0 eval chain b -> 10.0 stmt return + reuse calc test::Interleaved::p + eval chain a -> 1.0 eval feature ra -> 1.0 eval literal 1000.0 -> 1000.0 eval operator * -> 1000.0 diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_feature_binding.expected.json b/internal/exec/runtime/testdata/conformance/constraint_body_feature_binding.expected.json new file mode 100644 index 0000000000..e3dc60c671 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_feature_binding.expected.json @@ -0,0 +1,10 @@ +{ + "type": "instance", + "instantiate": "test::Subject", + "slots": { + "amount": {"type": "Integer", "value": 5} + }, + "constraints": { + "positive": true + } +} diff --git a/internal/exec/runtime/testdata/conformance/constraint_body_feature_binding.sysml b/internal/exec/runtime/testdata/conformance/constraint_body_feature_binding.sysml new file mode 100644 index 0000000000..e0071c7078 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/constraint_body_feature_binding.sysml @@ -0,0 +1,22 @@ +package test { + private import ScalarValues::*; + + // Constraint declarations are evaluated on each read in the current subject context. + constraint def PositiveAfterUpdate { + attribute twice : Integer = amount * 2; + twice > 3 + } + + part def Subject { + attribute amount : Integer := 0; + + action def Raise { + first start; + then action update { assign amount := 5; } + then done; + } + + perform action raise : Raise; + constraint positive : PositiveAfterUpdate; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_attribute_binding_reentry.expected.json b/internal/exec/runtime/testdata/conformance/state_attribute_binding_reentry.expected.json new file mode 100644 index 0000000000..d057904b77 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_attribute_binding_reentry.expected.json @@ -0,0 +1,12 @@ +{ + "type": "state", + "libraries": true, + "finalState": "done", + "stateVisits": ["active", "active", "done"], + "outputs": { + "source": {"type": "Integer", "value": 5}, + "seen1": {"type": "Integer", "value": 2}, + "seen2": {"type": "Integer", "value": 10}, + "visits": {"type": "Integer", "value": 2} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_attribute_binding_reentry.sysml b/internal/exec/runtime/testdata/conformance/state_attribute_binding_reentry.sysml new file mode 100644 index 0000000000..cc6daa782a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_attribute_binding_reentry.sysml @@ -0,0 +1,26 @@ +package test { + private import ScalarValues::*; + + state machine { + attribute source : Integer := 1; + attribute seen1 : Integer := 0; + attribute seen2 : Integer := 0; + attribute visits : Integer := 0; + + entry; then active; + state active { + attribute live : Integer = source * 2; + entry { + if visits == 0 { + assign seen1 := live; + } else { + assign seen2 := live; + } + then assign visits := visits + 1; + } + } + state done; + transition first active accept when visits == 1 do assign source := 5 then active; + transition first active accept when visits == 2 then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_attribute_body_feature_binding.expected.json b/internal/exec/runtime/testdata/conformance/state_attribute_body_feature_binding.expected.json new file mode 100644 index 0000000000..4b62f3563f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_attribute_body_feature_binding.expected.json @@ -0,0 +1,10 @@ +{ + "type": "state", + "finalState": "done", + "stateVisits": ["active", "done"], + "outputs": { + "source": {"type": "Integer", "value": 5}, + "observed": {"type": "Integer", "value": 10}, + "active.live": {"type": "Integer", "value": 10} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_attribute_body_feature_binding.sysml b/internal/exec/runtime/testdata/conformance/state_attribute_body_feature_binding.sysml new file mode 100644 index 0000000000..d595676999 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_attribute_body_feature_binding.sysml @@ -0,0 +1,19 @@ +package test { + private import ScalarValues::*; + + state Machine { + attribute source : Integer := 1; + attribute observed : Integer := 0; + + entry; then active; + state active { + attribute live : Integer = source * 2; + entry { + assign source := 5; + assign observed := live; + } + } + state done; + transition first active accept when observed > 0 then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_behavior_body_feature_bindings.expected.json b/internal/exec/runtime/testdata/conformance/state_behavior_body_feature_bindings.expected.json new file mode 100644 index 0000000000..ee1bc4817e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_behavior_body_feature_bindings.expected.json @@ -0,0 +1,9 @@ +{ + "type": "state", + "finalState": "done", + "stateVisits": ["active", "done"], + "outputs": { + "source": {"type": "Integer", "value": 4}, + "log": {"type": "Integer", "value": 234} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_behavior_body_feature_bindings.sysml b/internal/exec/runtime/testdata/conformance/state_behavior_body_feature_bindings.sysml new file mode 100644 index 0000000000..43f45184b4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_behavior_body_feature_bindings.sysml @@ -0,0 +1,31 @@ +package test { + private import ScalarValues::*; + + state Machine { + attribute source : Integer := 1; + attribute log : Integer := 0; + attribute complete : Boolean := false; + + entry; then active; + state active { + entry { + attribute entryValue : Integer = source; + assign source := 2; + then assign log := entryValue; + } + do { + attribute doValue : Integer = source; + assign source := 3; + then assign log := log * 10 + doValue; + then assign complete := true; + } + exit { + attribute exitValue : Integer = source; + assign source := 4; + then assign log := log * 10 + exitValue; + } + } + state done; + transition first active accept when complete then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_body_feature_binding_change_guard.expected.json b/internal/exec/runtime/testdata/conformance/state_body_feature_binding_change_guard.expected.json new file mode 100644 index 0000000000..9cd5ab6971 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_body_feature_binding_change_guard.expected.json @@ -0,0 +1,10 @@ +{ + "type": "state", + "finalState": "done", + "stateVisits": ["waiting", "done"], + "outputs": { + "a": {"type": "Integer", "value": 6}, + "s": {"type": "Integer", "value": 12}, + "observed": {"type": "Integer", "value": 12} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_body_feature_binding_change_guard.sysml b/internal/exec/runtime/testdata/conformance/state_body_feature_binding_change_guard.sysml new file mode 100644 index 0000000000..7e809a0613 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_body_feature_binding_change_guard.sysml @@ -0,0 +1,22 @@ +// The machine's transition polling observes a change in the live data binding. +package test { + private import ScalarValues::*; + + state Machine { + attribute a : Integer := 0; + attribute s : Integer = a * 2; + attribute observed : Integer := 0; + + entry; then waiting; + state waiting { + do action arm { assign a := 5; } + } + state done; + + transition first waiting accept when s > 3 do { + attribute effectValue : Integer = a * 2; + assign a := 6; + assign observed := effectValue; + } then done; + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_space_stop_time_binding.expected.json b/internal/exec/runtime/testdata/conformance/state_space_stop_time_binding.expected.json new file mode 100644 index 0000000000..687bcd736a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_space_stop_time_binding.expected.json @@ -0,0 +1,14 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::decay", + "outputs": { + "stateSpace": {"type": "VectorQuantity", "elements": [ + {"type": "Quantity", "value": 0.3584859224085422, "unit": "m"} + ]}, + "output": {"type": "VectorQuantity", "elements": [ + {"type": "Quantity", "value": 0.7169718448170844, "unit": "m"} + ]}, + "time": {"type": "Quantity", "value": 2.0, "unit": "s"} + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_space_stop_time_binding.sysml b/internal/exec/runtime/testdata/conformance/state_space_stop_time_binding.sysml new file mode 100644 index 0000000000..60c964bdb6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_space_stop_time_binding.sysml @@ -0,0 +1,34 @@ +package test { + private import ScalarValues::*; + private import SI::*; + private import VectorFunctions::*; + private import StateSpaceRepresentation::*; + private import StateSpaceIntegration::*; + + attribute def DecayState :> StateSpace; + attribute def DecayInput :> Input; + attribute def DecayOutput :> Output; + + action decay : ContinuousStateSpaceDynamics, FixedStepDynamics { + attribute rate : Real = 0.5; + in stop : DurationValue = 2 [s]; + in :>> input : DecayInput = 0 [m] * VectorOf((0.0)); + :>> stateSpace : DecayState = 1 [m] * VectorOf((1.0)); + :>> timeStep = 0.1 [s]; + :>> stopTime = stop; + + calc :>> getDerivative { + in input : DecayInput; + in stateSpace : DecayState; + return : StateDerivative = (0.0 - rate) * stateSpace / 1 [s]; + } + calc :>> getOutput { + in input : DecayInput; + in stateSpace : DecayState; + return : DecayOutput = 2.0 * stateSpace; + } + calc :>> getNextState { + calc :>> integrate : Euler; + } + } +} diff --git a/internal/exec/runtime/tool.go b/internal/exec/runtime/tool.go index 878fc5b380..60f189ac66 100644 --- a/internal/exec/runtime/tool.go +++ b/internal/exec/runtime/tool.go @@ -409,7 +409,10 @@ func (e *ActionExecutor) toolCall(execution *toolExecution) (*ToolCall, error) { reads := param.Direction == ast.DirIn || param.Direction == ast.DirInOut writes := param.Direction == ast.DirOut || param.Direction == ast.DirInOut if reads { - held, bound := e.root.data[e.root.key(name)] + held, bound, err := e.ctx.readBodyValue(e.root.cells, e.root.data, e.root.key(name)) + if err != nil { + return nil, err + } if !bound && !e.ctx.model.semantics.OptionalParameter(param.Symbol) { return nil, fmt.Errorf("%w: action %s: input parameter %s is bound by no argument", ErrUnboundParameter, symbolText(e.performed), name) diff --git a/internal/exec/runtime/via_reference.go b/internal/exec/runtime/via_reference.go index fcc7f67d27..4d95b4322e 100644 --- a/internal/exec/runtime/via_reference.go +++ b/internal/exec/runtime/via_reference.go @@ -41,7 +41,7 @@ func (ec *EvalContext) viaHolder(path string, viaSelf bool, self *Instance) (*In return self, path, err } if len(held) != 1 { - value, _ := ec.Lookup(root) + value, _, _ := ec.Lookup(root) return self, path, &SendTargetValueError{Target: path, Name: root, Value: FormatValue(value)} } holder := held[0] @@ -117,7 +117,10 @@ func (ec *EvalContext) boundEndDeliveries(end string) ([]ownerDelivery, bool, er // boundHolders returns the objects a binding of ec holds and whether ec binds the name; // a binding holding anything but objects is that error, naming the path it leads. func (ec *EvalContext) boundHolders(name, path string) ([]*Instance, bool, error) { - value, bound := ec.Lookup(name) + value, bound, err := ec.Lookup(name) + if err != nil { + return nil, false, err + } if !bound { return nil, false, nil } diff --git a/internal/exec/runtime/write_conformance.go b/internal/exec/runtime/write_conformance.go index d5a8c6c2a0..b05782d955 100644 --- a/internal/exec/runtime/write_conformance.go +++ b/internal/exec/runtime/write_conformance.go @@ -219,7 +219,7 @@ func storeBodyValue(ctx *Context, host stmtHost, env *stmtEnv, name string, valu if err := ctx.checkBodyWrite(host, s, &value); err != nil { return err } - env.data.set(name, value) + env.data.setBody(ctx, name, value) return nil } diff --git a/internal/ir/lower/action_graph.go b/internal/ir/lower/action_graph.go index 8513c2768d..b7202c24e4 100644 --- a/internal/ir/lower/action_graph.go +++ b/internal/ir/lower/action_graph.go @@ -344,10 +344,12 @@ func flattenChain(chain *ast.FeatureChainExpr) (ast.Node, []string) { // that block, so the executor binds it in the block's own frame and discards it // when the block exits. Value is nil when the declaration carried none. type Declare struct { - Name string - Value ast.Node - Node ast.Node // the declaration itself, for diagnostics - Scope *symbols.Scope // the scope the declaration was written in + Name string + Value ast.Node + Binding bool + BodyData bool // expression-valued state action execution stored as state data + Node ast.Node // the declaration itself, for diagnostics + Scope *symbols.Scope // the scope the declaration was written in } func (Declare) statement() { /* marker: closed Statement set */ } @@ -646,9 +648,10 @@ type Attribute struct { Direction ast.FeatureDirection IsResult bool // a `return` parameter, what the behavior yields // Type is the declared type as written (`Natural`, `Vehicle::Mode`), "" without one. - Type string - Value ast.Node - Node ast.Node // the declaration itself, for diagnostics + Type string + Value ast.Node + Binding bool + Node ast.Node // the declaration itself, for diagnostics // Scope is the scope the declaration was written in, in which its default // resolves; nil where the owner's own scope resolves it. Scope *symbols.Scope @@ -694,10 +697,16 @@ type Feature struct { Direction ast.FeatureDirection IsResult bool // a `return` parameter, what the node stands for read as a value Value ast.Node + Binding bool Node ast.Node // the declaration, for diagnostics Scope *symbols.Scope } +// valueIsBinding reports whether the declaration uses a live `=` value. +func valueIsBinding(value ast.Node, isInitial, isDefault bool) bool { + return value != nil && !isInitial && !isDefault +} + // Output reports whether the feature is written back rather than read: an `out` // or `return` parameter. func (f Feature) Output() bool { @@ -1400,6 +1409,7 @@ func declaredFeature(m *ast.Usage, scope *symbols.Scope) (Feature, bool) { Direction: m.Direction, IsResult: m.IsResult, Value: m.Value, + Binding: valueIsBinding(m.Value, m.ValueIsInitial, m.ValueIsDefault), Node: m, Scope: scope, }, true @@ -1924,7 +1934,7 @@ func lowerAttributes(members []ast.Node) []Attribute { if name == "" { continue } - attrs = append(attrs, Attribute{Name: name, Direction: usage.Direction, IsResult: usage.IsResult, Type: TypeText(usage), Value: usage.Value, Node: usage}) + attrs = append(attrs, Attribute{Name: name, Direction: usage.Direction, IsResult: usage.IsResult, Type: TypeText(usage), Value: usage.Value, Binding: valueIsBinding(usage.Value, usage.ValueIsInitial, usage.ValueIsDefault), Node: usage}) } return attrs } diff --git a/internal/ir/lower/body_binding_test.go b/internal/ir/lower/body_binding_test.go new file mode 100644 index 0000000000..812036c7da --- /dev/null +++ b/internal/ir/lower/body_binding_test.go @@ -0,0 +1,105 @@ +package lower + +import ( + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// TestLoweredBodyFeaturesPreserveValueOperators checks lowered action bindings and defaults. +func TestLoweredBodyFeaturesPreserveValueOperators(t *testing.T) { + graph := actionGraphFor(t, ` + action act { + attribute bound : Integer = 1; + attribute initial : Integer := 2; + attribute fallback : Integer default 3; + action nested { + attribute localBound : Integer = 4; + attribute localInitial : Integer := 5; + attribute localFallback : Integer default 6; + } + if true { + attribute blockBound : Integer = 7; + attribute blockInitial : Integer := 8; + attribute blockFallback : Integer default 9; + } + } + `) + + root := make(map[string]bool, len(graph.Attributes)) + for _, attr := range graph.Attributes { + root[attr.Name] = attr.Binding + } + if root["bound"] != true || root["initial"] || root["fallback"] { + t.Errorf("action attribute bindings = %v", root) + } + + var nested ast.Node + for _, node := range graph.Nodes { + if getNodeName(node) == "nested" { + nested = node + break + } + } + features := graph.Features[nested] + nestedBindings := make(map[string]bool, len(features)) + for _, feature := range features { + nestedBindings[feature.Name] = feature.Binding + } + if nestedBindings["localBound"] != true || nestedBindings["localInitial"] || nestedBindings["localFallback"] { + t.Errorf("nested action feature bindings = %v", nestedBindings) + } + + var local Declare + for _, node := range graph.Nodes { + for _, statement := range graph.Bodies[node] { + branch, ok := statement.(If) + if !ok { + continue + } + for _, nestedStatement := range branch.Then.Statements { + if declared, ok := nestedStatement.(Declare); ok && declared.Name == "blockBound" { + local = declared + } + } + } + } + if local.Name != "blockBound" || !local.Binding { + t.Fatalf("block binding declaration = %#v", local) + } +} + +// TestStateGraphPreservesAttributeValueOperators checks state attribute binding flags. +func TestStateGraphPreservesAttributeValueOperators(t *testing.T) { + graph, err := ToStateGraph(stateUsageIn(t, ` + state Machine { + attribute bound : Integer = 1; + attribute initial : Integer := 2; + attribute fallback : Integer default 3; + entry; then active; + state active { + attribute stateBound : Integer = 4; + } + } + `), nil) + if err != nil { + t.Fatalf("ToStateGraph: %v", err) + } + root := make(map[string]bool, len(graph.Attributes)) + for _, attr := range graph.Attributes { + root[attr.Name] = attr.Binding + } + if root["bound"] != true || root["initial"] || root["fallback"] { + t.Errorf("state machine attribute bindings = %v", root) + } + for state, attrs := range graph.StateAttributes { + if state.Name != "active" { + continue + } + if len(attrs) != 1 || attrs[0].Name != "stateBound" || !attrs[0].Binding { + t.Errorf("state attributes = %#v, want a binding stateBound", attrs) + } + return + } + t.Fatal("state attributes for active were not lowered") +} diff --git a/internal/ir/lower/calc_body.go b/internal/ir/lower/calc_body.go index 4a5ba72056..95cebabe0f 100644 --- a/internal/ir/lower/calc_body.go +++ b/internal/ir/lower/calc_body.go @@ -300,12 +300,12 @@ func usageStatement(u *ast.Usage, scope *symbols.Scope) (Statement, bool) { // An initial value (`:=`) is what the output holds when the body starts, // for its assignments to replace; a binding (`=`) is the value it returns. if u.ValueIsInitial && name != "" { - return Declare{Name: name, Value: u.Value, Node: u, Scope: scope}, true + return Declare{Name: name, Value: u.Value, Binding: valueIsBinding(u.Value, u.ValueIsInitial, u.ValueIsDefault), Node: u, Scope: scope}, true } return Return{Value: u.Value, Node: u, Scope: scope}, true } if u.Kind == ast.UsageAttribute && name != "" { - return Declare{Name: name, Value: u.Value, Node: u, Scope: scope}, true + return Declare{Name: name, Value: u.Value, Binding: valueIsBinding(u.Value, u.ValueIsInitial, u.ValueIsDefault), Node: u, Scope: scope}, true } if (u.Kind == ast.UsageCalc || u.Kind == ast.UsageAnalysisCase || u.Kind == ast.UsageVerificationCase) && name != "" { return DeclareUsage{Name: name, Node: u, Scope: scope}, true diff --git a/internal/ir/lower/state_behavior.go b/internal/ir/lower/state_behavior.go index d5b1b5d853..a37d02fa57 100644 --- a/internal/ir/lower/state_behavior.go +++ b/internal/ir/lower/state_behavior.go @@ -143,7 +143,7 @@ func lowerBehaviorBody(node *ast.Usage, scope *symbols.Scope, resolver *resolve. func lowerActionExecution(node *ast.ActionExecutionNode, scope *symbols.Scope) []Statement { switch { case node.Expression != nil: - return []Statement{Declare{Name: node.Name, Value: node.Expression, Node: node, Scope: scope}} + return []Statement{Declare{Name: node.Name, Value: node.Expression, BodyData: true, Node: node, Scope: scope}} case node.ActionRef != nil: return []Statement{Effect{Kind: EffectPerform, Node: node, Scope: scope}} default: diff --git a/internal/ir/lower/state_behavior_test.go b/internal/ir/lower/state_behavior_test.go index 71d72b3cf0..20c4f50454 100644 --- a/internal/ir/lower/state_behavior_test.go +++ b/internal/ir/lower/state_behavior_test.go @@ -3,6 +3,8 @@ package lower import ( "strings" "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) // The inline body of an entry, do or exit behavior stating successions or @@ -169,3 +171,18 @@ func TestStateBehaviorBodyWithAmbiguousStartKeepsNoInitial(t *testing.T) { } } } + +// TestStateBehaviorActionExecutionIsStateData checks lowered action results in state data. +func TestStateBehaviorActionExecutionIsStateData(t *testing.T) { + body := lowerActionExecution(&ast.ActionExecutionNode{ + Name: "calculated", + Expression: &ast.LiteralInteger{Value: "2"}, + }, nil) + if len(body) != 1 { + t.Fatalf("action execution = %d statements, want a declaration", len(body)) + } + decl, ok := body[0].(Declare) + if !ok || !decl.BodyData || decl.Name != "calculated" { + t.Errorf("action execution = %#v, want calculated declared as state data", body[0]) + } +} diff --git a/internal/ir/lower/state_graph.go b/internal/ir/lower/state_graph.go index 44d5289c92..79d8987ad9 100644 --- a/internal/ir/lower/state_graph.go +++ b/internal/ir/lower/state_graph.go @@ -477,7 +477,7 @@ func lowerStateAttributes(graph *StateGraph, members []inheritedMember) []Attrib continue } graph.attributeScope[usage] = member.scope - attrs = append(attrs, Attribute{Name: name, Direction: usage.Direction, IsResult: usage.IsResult, Type: TypeText(usage), Value: usage.Value, Node: usage, Scope: member.scope}) + attrs = append(attrs, Attribute{Name: name, Direction: usage.Direction, IsResult: usage.IsResult, Type: TypeText(usage), Value: usage.Value, Binding: valueIsBinding(usage.Value, usage.ValueIsInitial, usage.ValueIsDefault), Node: usage, Scope: member.scope}) } return attrs } diff --git a/internal/ir/lower/state_inheritance.go b/internal/ir/lower/state_inheritance.go index 8df68a70a3..2b9f816c2b 100644 --- a/internal/ir/lower/state_inheritance.go +++ b/internal/ir/lower/state_inheritance.go @@ -397,7 +397,7 @@ func (g *StateGraph) addMember(content *stateContent, member ast.Node, parallel // A restated run-to-completion default is what the executor implements, not a slot. case isStateDatum(m): if name, _ := ast.EffectiveName(m); name != "" { - content.attrs = append(content.attrs, Attribute{Name: name, Direction: m.Direction, IsResult: m.IsResult, Type: TypeText(m), Value: m.Value, Node: m, Scope: scope}) + content.attrs = append(content.attrs, Attribute{Name: name, Direction: m.Direction, IsResult: m.IsResult, Type: TypeText(m), Value: m.Value, Binding: valueIsBinding(m.Value, m.ValueIsInitial, m.ValueIsDefault), Node: m, Scope: scope}) g.attributeScope[m] = scope } default: diff --git a/tests/parser/testdata/parse/body_feature_bindings.golden b/tests/parser/testdata/parse/body_feature_bindings.golden new file mode 100644 index 0000000000..e4b66ad7f4 --- /dev/null +++ b/tests/parser/testdata/parse/body_feature_bindings.golden @@ -0,0 +1,98 @@ +(RootNamespace + (Membership visibility="default" + (Package name="BodyFeatureBindings" library=false standard=false + (Import visibility="private" all=false kind=namespace recursive=false imported="ScalarValues" filtered=false) + (Membership visibility="default" + (Definition kind="action" abstract=false variation=false name="Work" + (Membership visibility="default" + (Usage kind="attribute" name="source" ref=false direction="none" composite=false derived=false ordered=false nonunique=false initial=true + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (LiteralInteger value="1"))) + (Membership visibility="default" + (Usage kind="attribute" name="live" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (OperatorExpr operator="*" + (FeatureReference name="source") + (LiteralInteger value="2")))) + (Membership visibility="default" + (Usage kind="attribute" name="fallback" ref=false direction="none" composite=false derived=false ordered=false nonunique=false default=true + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (FeatureReference name="source"))) + (Membership visibility="default" + (Usage kind="action" name="nested" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Membership visibility="default" + (Usage kind="attribute" name="nestedLive" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (OperatorExpr operator="+" + (FeatureReference name="source") + (LiteralInteger value="1")))))) + (IfActionNode + (OperatorExpr operator=">" + (FeatureReference name="source") + (LiteralInteger value="0")) + (IfBranchNode kind="then" + (Membership visibility="default" + (Usage kind="attribute" name="blockLive" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (OperatorExpr operator="*" + (FeatureReference name="source") + (LiteralInteger value="3")))) + (*ast.AssignmentActionNode))))) + (Membership visibility="default" + (Definition kind="state" abstract=false variation=false name="Machine" + (Membership visibility="default" + (Usage kind="attribute" name="source" ref=false direction="none" composite=false derived=false ordered=false nonunique=false initial=true + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (LiteralInteger value="1"))) + (Membership visibility="default" + (Usage kind="attribute" name="live" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (OperatorExpr operator="*" + (FeatureReference name="source") + (LiteralInteger value="2")))) + (Membership visibility="default" + (Usage kind="state" name="active" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Membership visibility="default" + (Usage kind="attribute" name="stateLive" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (OperatorExpr operator="+" + (FeatureReference name="source") + (LiteralInteger value="1")))) + (EntryMember + (Membership visibility="default" + (Usage kind="action" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false prefix="entry" + (Membership visibility="default" + (Usage kind="attribute" name="entryLive" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (OperatorExpr operator="*" + (FeatureReference name="source") + (LiteralInteger value="3"))))))) + (DoMember + (Membership visibility="default" + (Usage kind="action" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false prefix="do" + (Membership visibility="default" + (Usage kind="attribute" name="doLive" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (OperatorExpr operator="*" + (FeatureReference name="source") + (LiteralInteger value="4"))))))) + (ExitMember + (Membership visibility="default" + (Usage kind="action" name="" ref=false direction="none" composite=false derived=false ordered=false nonunique=false prefix="exit" + (Membership visibility="default" + (Usage kind="attribute" name="exitLive" ref=false direction="none" composite=false derived=false ordered=false nonunique=false + (Relationship kind="typing" target=Integer + (*ast.QualifiedName)) + (OperatorExpr operator="*" + (FeatureReference name="source") + (LiteralInteger value="5")))))))))))))) \ No newline at end of file diff --git a/tests/parser/testdata/parse/body_feature_bindings.sysml b/tests/parser/testdata/parse/body_feature_bindings.sysml new file mode 100644 index 0000000000..31ceb9d3f6 --- /dev/null +++ b/tests/parser/testdata/parse/body_feature_bindings.sysml @@ -0,0 +1,36 @@ +package BodyFeatureBindings { + private import ScalarValues::*; + + action def Work { + attribute source : Integer := 1; + attribute live : Integer = source * 2; + attribute fallback : Integer default source; + + action nested { + attribute nestedLive : Integer = source + 1; + } + + if source > 0 { + attribute blockLive : Integer = source * 3; + assign source := 2; + } + } + + state def Machine { + attribute source : Integer := 1; + attribute live : Integer = source * 2; + + state active { + attribute stateLive : Integer = source + 1; + entry { + attribute entryLive : Integer = source * 3; + } + do { + attribute doLive : Integer = source * 4; + } + exit { + attribute exitLive : Integer = source * 5; + } + } + } +}