Skip to content

fix(runtime): read a behavior's own features from its performance in body expressions - #1511

Merged
HuiJun merged 3 commits into
developfrom
fix/held-image-behavior-occurrence-attributes
Oct 25, 2026
Merged

HuiJun merged 3 commits into
developfrom
fix/held-image-behavior-occurrence-attributes

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What and why

An attribute a state or action definition declares over its own occurrence features — size(entryAction), size(en), size(doAction), size(subperformances), a nested part fresh — read the wrong object, or nothing, whenever it was evaluated through the behavior's own evaluation context. A held-image copy of an exhibited machine with attribute n : Integer = entryAction->size(); answered no value for feature entryAction and its state data read {}; the same machine live failed the same way as soon as a feature the expression also reads was written (attribute total = size(entryAction) + offset; reads 2, then assign offset := 2 leaves total unreadable); and size(subperformances) or size(enclosedPerformances) in a state definition counted the exhibiting part's performances (0) while sm.subperformances read directly held one.

The root is in the evaluation of names, not in the restore order. evalNameGeneral read a bare name from the frames, then from the performer (self) by name, then from the declaration's scope — never from the performance's own occurrence, which the state machine's cell contexts (ensureStateCells, stateAttributeContext, the step and trigger contexts) did not even carry. Live this was masked: initializeAttributes seeds a binding cell from the occurrence's already-materialized feature value, so the expression was never evaluated through the machine's context until something made the tracking cell derive again — a held-image copy (tracking values restore unmaterialized and re-derive lazily, as documented) or a written dependency.

The fix is general and names no entry, do or exit action:

  • StateExecutor.newEvalContext builds every machine evaluation context with the machine's occurrence beside thisOccurrence, as the action performances' evalContextFor already did.
  • EvalContext.performanceFeatureValue reads a name that resolves to a feature the performance's type holds from the performance occurrence, after the frames and before the performer's same-named feature. A feature a frame holds without a value (an unsupplied parameter) stays the frame's to answer, so bind noting.n = level inside a performed action still reads the action's empty level, not the part's.
  • selfFeatureValue is the same read on self (featureValueOn).
  • A feature chain and a chained assignment target whose base is such a usage (fresh.n, assign fresh.n := 5 inside the performed go), and a qualified read of one (SM::entryAction), start from the performance's own part too (evalFeatureChain, chainRoot, evalNameGeneral's qualified path), so a bare fresh, fresh.n and a write to fresh.n name one object; before, fresh.n read and wrote an object of the bare declaration (#4 fresh owner=#0) beside the performance's.

No change to held_image_behavior.go: the restore path was re-deriving correctly through a context that could not see the occurrence.

Behaviour changes

probe (T::p exhibits sm : SM, performs ad : AD) develop 111a59a branch
sm.n = size(entryAction), sm.m = size(en), sm.d = size(doAction), sm.x = size(exitAction), sm.sa = size(subactions) live 1, 1, 1, 1, 1 same
sm.sp = size(subperformances), sm.ep = size(enclosedPerformances) live 0, 0 (the part's) 1, 1 (T::p.sm.subperformances reads [#4] on both)
the seven attributes in a held-image copy (StateDataWithError) no value for feature entryAction, data {} all seven, equal to the source
total = size(entryAction) + offset after a transition assigns offset := 2 (live) no value for feature entryAction 3
ad.sa = size(subactions) on an action def, live and in a copy 2 2
ad.sp, ad.ep on an action def, live no value for feature sp / ep unchanged (instance-level read of a performed action's subperformances; not touched here)
size(triggerCount)-style reads of empty [0..1] features (incomingTransitionTrigger, accepted) in a copy n/a (copy unreadable) 0, 0
detached CreateStateExecutor(SM): size(entryAction) 1 1
OccurrencesTest::p1.v, new OccurrencesTest::P1().v 2, 2 2, 2
fresh.n read, then assign fresh.n := 5, then fresh.n again, inside the performed action 1, 5 — on a declaration-level object (#4 fresh owner=#0), go.fresh untouched 1, 5 — on go.fresh (#3 Widget owner=#2.fresh), no ownerless object
part fresh / part spare named inside a performed action (occurrence_lifecycle_in_performed_action) a declaration-level object (#3 fresh owner=#0); the performance's own go.fresh slot stays <unknown> the performance's own go.fresh (#3 Widget owner=#2.fresh); same ids, same values

Golden and fixture movement

  • conformance/occurrence_lifecycle_in_performed_action.trace.golden: 4 lines, materialize: fresh #3 / create: fresh #3 / materialize: spare #5 / create: spare #5 → Widget #3 / Widget #5 (regenerated with -update-traces): the body's fresh now evaluates to the performance's go.fresh feature value, which the trace labels by type, instead of an object of the bare declaration labelled by its name. No value in the case moved.
  • No other golden, baseline, sweep row or corpus expectation moved (TestKernelLibraryMembersReadableWhileRunning, TestLibraryAndPrefixSweep, the corpora ratchets and training_examples_expected.txt unchanged).
  • New conformance/behavior_occurrence_attributes fails on develop at subperformancesAgree/enclosedAgree (false) and in TestHeldImageRoundTrip (the copy differs from the graph imaged); passes here. The two Kernel-collection counts are pinned as agreement with the part's size(modes.subperformances) rather than as 1, because fix(runtime): derive implicit specializations for every performance the run creates #1492 derives the collections from every member held (sm.subperformances = 3 there); the fixture passes on this branch merged with fix(runtime): derive implicit specializations for every performance the run creates #1492.

Not in this PR

  • ad.sp/ad.ep: size(subperformances) declared on an action def read through the part fails no value for feature sp on develop and here (instance-level derivation of a performed action's Kernel collections); size(subactions) works.
  • An exhibited machine's unsupplied in gauge : Integer[0..1] reads <undetermined> in size(gauge) on develop and here; the occurrence's slot is undetermined too, so such an object cannot be imaged (a undetermined the model does not determine).

Specification basis

KerML 1.0 §7.4.11 Feature Values: a feature value's implicit binding connector has "the same featuring types as the declared feature", so attribute n = size(entryAction) declared in state def SM binds within each SM performance, between its own n and its own entryAction; KerML 1.0 §8.2.3.5 name resolution: entryAction, subperformances written in SM's body resolve to SM's (inherited) members, never to the exhibiting part's same-named features. New row in docs/project/spec-compliance.md after the state entry/do/exit multiplicity row.

How it was verified

  • conformance/behavior_occurrence_attributes (state def: entryAction, en, doAction, exitAction, subactions counts, subperformances/enclosedPerformances agreement with the part's read; action def: subactions), carried by TestExecutionConformance, TestExecutionConformanceUnderPolicies and TestHeldImageRoundTrip
  • held_image_behavior_attributes_test.go:TestHeldImageRederivesBehaviorOccurrenceAttributes: the copy's StateDataWithError is error-free and equal to the source's seven values; the copied performed action's subactionCount cell reads 2
  • robustness_behavior_occurrence_attributes_test.go:TestRuntimeRobustnessBehaviorOccurrenceAttributes: an optional occurrence feature empty after restore reads 0 in source and copy; a tracking attribute re-derives to 3 after a transition writes a feature it reads, live and in a copy of the moved machine; a chained read and a chained write (fresh.n) inside a performed action name the performance's own part and leave no ownerless object; an attribute over a valueless feature is a typed ErrNoValue naming the feature, never a panic
  • the unset guard is pinned by the existing conformance/performed_action_binding_end_names_parameter, TestRuntimeRobustnessBindingEndNames and TestRuntimeRobustnessStepParameter, which fail without it
  • held-image round-trip cost on the existing fixtures (TestHeldImageRoundTrip, wall time, best of three): develop 1.54 s / 1.57 s / 1.69 s, branch 1.54 s / 1.59 s / 1.70 s (the branch suite carries one fixture more), unchanged within noise
  • go build ./..., go vet ./..., gofmt -l . empty, make lint (staticcheck + gosec) clean, go test ./internal/exec/runtime (targeted TestExecutionConformance|TestHeldImage|TestKernelLibraryMembersReadableWhileRunning|TestRuntimeRobustness included), go test ./... with the training, pilot, spec-example and pilot-library-XMI corpora downloaded and OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_SPEC_EXAMPLES=1 OPENSYSML_REQUIRE_PILOT_LIBRARY_XMI=1 set: all packages ok (cmd/sysml 294 s, tests/codegen 325 s, tests/corpus 45 s, tests/identity, internal/exec/runtime 179 s)
  • python3 scripts/changelog.py check, python3 scripts/check-doc-ids.py, make docs-counts current
  • dry-merged against origin/develop, fix(runtime): derive implicit specializations for every performance the run creates #1492 fix/implicit-specialization-runtime-objects and test(runtime): pin a state's entry, do and exit actions as one performance each #1507 test/state-action-default-performances: no conflicts; the new fixture and tests pass on the tree merged with fix(runtime): derive implicit specializations for every performance the run creates #1492

Checklist

  • make test and make lint pass locally
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (see CONTRIBUTING.md)
  • Changelog entry added as changes/unreleased/<slug>.<section>.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (compliance rows need nothing: the census is counted at docs build)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/56a7606a30964a9c969dbbd909b120ef
Open in Devin Desktop: https://nasa-jpl-demo.devinenterprise.com/desktop/session/56a7606a30964a9c969dbbd909b120ef?variant=devin
Requested by: @HuiJun

devin-ai-integration Bot and others added 2 commits October 25, 2026 11:25
…body expressions

An attribute a state or action definition declares over its own occurrence
features (size(entryAction), size(en), size(subperformances), a nested part)
reads the performance's feature value, not the performer's same-named feature
nor an object of the bare declaration. The state machine's evaluation contexts
carry the machine's occurrence, so a tracking cell re-derives such an attribute
after a feature it reads is written and in a held-image copy, which no longer
answers 'no value for feature entryAction'.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nces counts by agreement with the part's read

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review October 25, 2026 11:53
devin-ai-integration[bot]

This comment was marked as resolved.

…formed from its performance

A chain or assignment target starting at a part the body declares (fresh.n)
reaches the part the performance holds, as a bare read of it does, instead of
an object of the bare declaration.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

9831f6c extends the performance-first read to the two paths that bypass the bare-name lookup: a feature chain and a chained assignment target whose base is a usage the behavior declares (fresh.n, assign fresh.n := 5 inside the performed go, through evalFeatureChain and chainRoot), and a qualified read of such a feature (SM::entryAction, the currentSym path), all through one performanceFeatureOf(sym). Before it a chained read or write materialized an object of the bare declaration beside the performance's own part, so fresh and fresh.n named two objects; now they name one and no ownerless object is left. The two comments the review flagged are two lines each. New robustness subtest chained_read_names_the_performances_part; the PR body carries the new row. Gates re-run on this head: go vet, gofmt -l . empty, make lint, go test ./... with the four corpora and their OPENSYSML_REQUIRE_* set, all green; dry-merge against develop (111a59a), #1492 and #1507 still clean.

@HuiJun
HuiJun merged commit c922d29 into develop Oct 25, 2026
30 checks passed
@HuiJun
HuiJun deleted the fix/held-image-behavior-occurrence-attributes branch October 25, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant