Skip to content

fix(runtime): judge a behavior's write count by the feature's effective multiplicity - #1556

Merged
HuiJun merged 3 commits into
developfrom
fix/write-default-multiplicity
Oct 27, 2026
Merged

HuiJun merged 3 commits into
developfrom
fix/write-default-multiplicity

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown
Contributor

What and why

A behavior writing a feature it declares judged the value's count unconditionally: Context.checkNamedWriteHeld (reached from ActionExecutor.setFeature and StateExecutor.assignAttribute through checkNamedWriteAs) and Context.checkWrite (tool outputs) passed countJudged = true to checkTargetHeld, so a feature stating and inheriting no multiplicity — writeTargetIn gave it the representative 1..1 — refused a second value, while the same write to a part's feature (Instance.SetFeatureValue → Context.checkAdmits) consulted EffectiveFeature.MultiplicityDefaulted and held both.

Before, on develop 0286c9845:

  • part def P { attribute many : Real[0..*]; attribute x : Real :> many; action a { assign x := (3.0, 4.0); } } — the part's x holds (3.0, 4.0).
  • action def A { attribute many : Real[0..*]; attribute x : Real :> many; assign x := (3.0, 4.0); } — refused: action A: assignment to x: multiplicity violation: 2 value(s) bound to a feature with multiplicity upper bound 1.
  • The same features on a state def, written by an entry action — refused the same way (state machine S: assignment to x: …).

After: all three hold both values. attribute z : Real; (implicit [1..1]) still refuses (3.0, 4.0) on all three paths; a stated [0..*] holds three values and a stated [1..2] holds two and refuses one or three, on every path.

The fix takes the count judgment from one place:

  • runtime/shape.go featureRange now also reports whether a declaration, inheritance or the parameter rule states the range (beside defaulted, unchanged in meaning).
  • runtime/write_conformance.go: new Context.writeTargetOf(sym, name) builds a writeTarget from featureRange — the same decision effectiveFeature gives an object's feature and checkAdmits judges — setting countJudged = !defaulted (a write's count rule) and stated (the existing body-local declaration rule: a local omitting a multiplicity keeps its initializer's count; that rule is unchanged, it just has its own flag now). writeTargetIn and toolOutput use it. checkTarget/checkTargetAs/checkTargetHeld lost the countJudged parameter and read target.countJudged; checkBodyDeclarationHeld, shapeDeclared and the compiled tier's localCheckFor read target.stated, as they did before under the old flag's meaning.
  • Other writers audited: REPL %set and gRPC SetFeatureValue go through Instance.SetFeatureValue → checkAdmits (already right); calc/out assignment and bindCalcParameters go through checkNamedWriteHeld (now unified) and calcMemberDecl.check (declaration-based, unchanged); generated code (codegen.compileDeclare) declines a subsetting local as unsupported and checks only a stated multiplicity on declarations, so it has no write check of its own to bring in line — no codegen parity case was added for that reason.

Specification basis

  • KerML 1.1 Beta 2 §7.4.12 (a feature without a declared multiplicity has the default 0..*), §7.3.4.5 (a redefining/subsetting feature stating none keeps the one it inherits — x :> many is [0..*]).
  • SysML v2.1 Beta 2 §7.6.3 (an eligible usage without a declared multiplicity has the implicit [1..1]): attribute z : Real; keeps refusing two values.

docs/project/spec-compliance.md: the write-conformance type row (impl cell: writeTargetIn → writeTargetOf, one featureRange decision; text: the element-count sentence now states the unified rule for part/action/state defs) and the write-count row (text, impl, tests). No status marker moves.

Against #1550 (docs/representative-object-policy, merged into develop at 48f60b4b8): both PRs edited the type row. The merge commit d380d5dc0 (origin/develop 48f60b4b8 merged in, one conflict on that row) resolves it to the row as it reads after both: #1550's rewritten element-count sentence with its split clause ("…while the same declaration owned by the running action itself still refuses two values … a split recorded in the status rather than claimed away") replaced by this PR's unified wording, this PR's impl cell, and #1550's status cell minus its "count-free … honoured on one path and not yet on the other" sentence.

How it was verified

New fixtures, side by side:

  • conformance/write_default_multiplicity_part_def.sysml, write_default_multiplicity_action_def.sysml, write_default_multiplicity_state_def.sysml — a subsetting feature holds two values, a stated [0..*] three, a stated [1..2] two. The action-def and state-def forms fail on develop with the refusal above (checked by stashing the code change).
  • conformance/write_implicit_one_part_def.sysml, write_implicit_one_action_def.sysml, write_implicit_one_state_def.sysml — two values into an implicit-[1..1] attribute refused on each path.
  • runtime/robustness_write_default_multiplicity_test.go (TestRuntimeRobustnessWriteDefaultMultiplicity): defaulted subset and stated [0..*] hold many; implicit one refuses two with ErrMultiplicityViolation; [1..2] refuses three and none, holds two.

Baselines: none moved. A first cut that judged body-local declarations by the implicit [1..1] too moved calc_sample_pair_arithmetic, calc_integer_range, calc_body_local_declared_type, the analysis-library census (SampledFunctions::interpolateLinear declares attribute domainValues = … with two values), terminate_freezes_root_binding and the clock_accept_after_parameter trace — i.e. the declaration rule is load-bearing for the standard library itself — so the declaration rule keeps its own flag and nothing moves.

Gates on 7be167161 (the fix) and re-run on d380d5dc0 (after merging develop 48f60b4b8); c0931c13d on top only shortens three comments (gofmt, vet, the new fixtures and robustness test re-run ✔):

  • go build ./... ✔, go vet ./... ✔, gofmt -l . empty ✔ (both heads)
  • go test -count=1 ./internal/exec/runtime/... ✔ (156 s on 7be167161; conformance, robustness, trace, census and tool suites re-run ✔ on d380d5dc0)
  • python3 scripts/changelog.py check ✔, mkdocs build --strict ✔ (both heads)
  • go test -count=1 ./... on 7be167161 with the five corpora fetched and their OPENSYSML_REQUIRE_* set: 121 packages ok, cmd/sysml hit the default 10 m package timeout while the whole module ran in parallel on this machine (the test it was starting, TestSyncAnnotateAddressesByShortName, was at 0 s — a CPU-contention timeout, not a failure; the package touches nothing this PR changes); re-run alone it passes (ok cmd/sysml 275 s), and the whole module re-run on d380d5dc0 with the same OPENSYSML_REQUIRE_* set and -timeout 30m passes: 122 packages ok, no failures.

Dry-merges (detached worktrees of this head):

Checklist

  • make test and make lint pass locally (build, vet, gofmt, full go test ./... with corpora, see above)
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (see CONTRIBUTING.md)
  • Changelog entry added as changes/unreleased/write-default-multiplicity.fixed.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved — nothing moved
  • No internal work-item labels in the body, docs, or changelog

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

…ve multiplicity

A behavior writing a feature it declares took countJudged = true unconditionally
in checkNamedWriteHeld and checkWrite, so an action or state definition's own
attribute subsetting a [0..*] feature refused a second value against the
representative one while the same write to a part's feature (checkAdmits,
MultiplicityDefaulted) held both. writeTargetOf now takes the multiplicity and
the count judgment from featureRange, the decision effectiveFeature already
takes; writeTarget.stated keeps the declaration rule for body-locals.

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

Resolves the write-conformance type row of docs/project/spec-compliance.md
as it reads after the representative-object policy docs and this fix.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review October 26, 2026 23:17
devin-ai-integration[bot]

This comment was marked as resolved.

…o lines

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@HuiJun
HuiJun merged commit 25c9d42 into develop Oct 27, 2026
30 checks passed
@HuiJun
HuiJun deleted the fix/write-default-multiplicity branch October 27, 2026 00:36
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