Skip to content

fix(migrate): write an empty transition effect the parser accepts and gate the PSSM suite - #485

Merged
HuiJun merged 3 commits into
developfrom
fix/migrate-empty-effect-body
Sep 21, 2026
Merged

HuiJun merged 3 commits into
developfrom
fix/migrate-empty-effect-body

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

What and why

bin/sysml model.xmi -convert sysml wrote a transition whose effect is an Activity with no nodes as

transition 'T1.2' first 'S1.1'
    do action effect;
    then done;

The parser rejects the ; (expected the target of the transition after 'then'), and one such transition made the whole migrated file unwritable — the OMG PSSM test suite (47k elements) failed at output line 12343 on a single empty effect.

Root cause

writer.block(header, body) writes header; when the body writes nothing. That is right for every standalone declaration, but a transition's do <action-usage> is a clause: the grammar continues with then <target> on the next line, and ; ends the transition instead. Only that one site was affected; the fix is a second helper that keeps an empty brace pair and a context flag in the behavior writer:

func (w *writer) block(header string, body func())   // empty body → `header;`
func (w *writer) braced(header string, body func())  // empty body → `header { }`

inlineBehavior picks braced when the owner is a Transition and the keyword is do action, and block for a state's entry/do/exit action, which stands alone. A referenced effect likewise drops the ;: do action : Def then then t;.

Notation per shape (state action / transition effect)

Shape State entry / do / exit Transition do Report
Activity with no nodes entry action x; do action x { } mapped, note "the activity has no nodes, so the action is empty"
Activity whose every action node is refused entry action x { /* … */ } do action x { /* … */ } (flow lines and one comment per refused node) approximated, "none of its N actions is migrated, so it runs nothing"; each node unmapped
OpaqueBehavior in a language the mapping cannot write entry action x { /* body … */ } do action x { /* body … */ } approximated, body kept as a comment
Behavior owned elsewhere (referenced) entry x : Def; (unchanged) do action : Def mapped, note "also run as the do action of …"
Referenced behavior that is not written, or is written as something no state runs (a StateMachine) comment in the state body comment before then the state or transition approximated, "its … is not run: …"; a behavior not written unmapped

The same shapes apply to a guard-less completion transition, which shares writeTransitionEffect.

do action x { } was chosen over dropping the do clause: an Activity with no nodes is an empty behavior, so writing an empty action is the faithful mapping and the report can stay Mapped with a note, whereas omitting the clause would have to be reported approximated for a construct that lost nothing.

A second defect surfaced by the referenced-effect fixture: a transition whose effect is a Behavior owned elsewhere (effect="_id" rather than an owned <effect> child) was silently dropped — firstOwned(t, "effect") found nothing. behaviorIn(t, "effect"), the lookup already used for a state's entry/do/exit, now resolves both forms, in states.go and in the carrier check that decides whether a transition's effect is written inline.

Audit of head + optional body

Every w.block site was checked for a body that may be empty and a continuation that follows on the next line (w.block has 39 call sites across activity.go, behavior.go, interaction.go, migrate.go, simconfig.go, states.go). The transition do clause was the only one: every other block writes a definition, usage, package, state, region, or action inside a body, and each of those is complete at header;. The transition writer's own braced("do action", …) (the accept keep-alive when there is no effect) always writes a line, so it was never empty, but it uses braced too so the site cannot regress if the body ever becomes optional.

PSSM corpus gate

tests/corpus/pssm_migration_test.go (TestPSSMSuiteMigration) migrates build/pssm/PSSM_TestSuite.xmi through the same convert.Migrate the CLI uses, and

  • asserts zero syntax errors — a *convert.SyntaxError fails the test;
  • ratchets the report's mapped/approximated/unmapped/skipped totals and the validation-error count of the migrated notation against tests/corpus/testdata/pssm_migration_expected.txt, regenerated with -update-pssm-migration (any movement must be adjudicated in the PR, as for the pilot corpora);
  • skips while the suite is absent, and fails instead under OPENSYSML_REQUIRE_PSSM_SUITE=1, through the shared corpusGate in corpus_gate_test.go.

The validation-error count (21) is recorded because it was identical across repeated runs and across cache states (a fresh XDG_CACHE_HOME is used). It is a ratchet, not an assertion: the 21 are unresolved-member and junction errors from unrelated refusals, described in docs/project/pssm-migration.md.

CI downloads the suite with ./scripts/download-pssm-suite.sh (the pin in tools/referee/pssm/pin.go, unchanged) and runs the gate with OPENSYSML_REQUIRE_PSSM_SUITE=1 in both .circleci/config.yml and .github/workflows/pr.yml, mirroring the pilot corpora steps. tools/referee/pssm/ is not touched.

Docs

  • docs/reference/sysml-v1-migration.md: five new mapping rows (the three empty/refused/unsupported shapes, the referenced transition effect, and a referenced behavior that cannot be run).
  • docs/project/pssm-migration.md (new, linked from docs/project/README.md and mkdocs.yml), docs/reference/environment.md, CONTRIBUTING.md, AGENTS.md: the gate, its variable and its update flag, beside the pilot-corpora entries.
  • changes/unreleased/migrate-empty-effect-body.fixed.md.

states.go changes are confined to inlineBehavior, writeTransitionEffect and the behaviorIn doc comment, so they merge cleanly with the composite-state entry/exit work.

Specification basis

OMG SysML v2 textual notation, TransitionUsage: transition [first source] [accept …] [if …] [do <ActionUsage>] then <target>; — the effect action usage has no terminator of its own; the transition's ; follows the target. internal/syntax/parser (the transition rule) and the golden ASTs under tests/parser/testdata/parse/ fix the accepted forms: do action x { } and do action : Def continue into then, do action x; ends the transition. No row in docs/project/spec-compliance.md moves; this is the v1→v2 mapping, tracked in docs/reference/sysml-v1-migration.md.

How it was verified

New tests:

  • internal/translate/migrate/writer_test.go TestWriterBracedKeepsEmptyBody: braced keeps { } for an empty body and braces a non-empty one; block still writes ;.
  • tests/migrate/testdata/xmi/empty_behaviors.xmi (hand-written, non-TMT) with .golden.sysml and .golden.report.txt: a state machine whose states carry empty, all-refused (CreateObjectAction/DestroyObjectAction) and unsupported-language (Fortran OpaqueBehavior) entry/do/exit behaviors, the same three shapes as effects of triggered and of guard-less completion transitions, a class-owned Activity referenced as the effect of one triggered and one completion transition, and a class-owned StateMachine that a state names as its entry and a transition as its effect, which no state can run: both owners are reported approximated with the reason, where the state was previously left mapped over a comment. TestGoldenConstructFixtures already opens every migrated golden in a workspace and fails on any diagnostic error, so the golden is asserted to parse and validate clean (checked: it does for every fixture under tests/migrate/testdata/xmi/). Report: 60 elements, 37 mapped, 17 approximated, 6 unmapped (the six refused action nodes).
  • tests/corpus/pssm_migration_test.go as above.

Gates (all green on this branch):

go build ./... && go vet ./... && gofmt -l .        # clean, gofmt prints nothing
go test ./...                                       # ok
go test -C tools ./... && go vet -C tools ./...     # ok
./scripts/download-training-examples.sh && ./scripts/download-pilot-corpora.sh && ./scripts/download-pilot-library-xmi.sh
OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_PILOT_LIBRARY_XMI=1 OPENSYSML_REQUIRE_PSSM_SUITE=1 go test -count=1 ./...   # ok
make lint && make docs-check && make man-check      # ok
python3 scripts/changelog.py check && python3 scripts/check-doc-ids.py   # OK

No pilot-corpus, training-corpus or round-trip ratchet moved.

End-to-end, PSSM (./scripts/download-pssm-suite.sh && bin/sysml build/pssm/PSSM_TestSuite.xmi -convert sysml -migration-report /tmp/pssm-report.txt -o /tmp/pssm.sysml, then bin/sysml /tmp/pssm.sysml -validate; the binary has no -check, -validate is the validation surface):

before after
migration fails: the migrated notation could not be written: …:12343:41: expected the target of the transition after 'then': 1 syntax error(s) writes 5,007,255 bytes in about a second
syntax errors in the output 1 0
validation errors not reachable (file not written) 21
validation warnings not reachable ~2,900 (duplicate inherited member names, inherited from the suite's SemanticTest layout)
report: mapped / approximated / unmapped / skipped 23,512 / 13,562 / 10,229 / 6 (the CLI wrote no report, since the conversion failed; taken from migrate.Migrate on develop) 23,512 / 13,562 / 10,229 / 6

Report totals do not move: the one empty effect was already Mapped; its note now says the action is empty. The suite has no transition with a referenced effect, no effect whose every node is refused, and no unsupported-language effect, so the approximated count is unchanged too.

TMT (bin/sysml <TMT>.mdzip -convert sysml -migration-report … -o …, then -validate), this branch against develop:

before after
migrated notation byte-identical on both
validation errors 0 0
report: mapped / approximated / unmapped 28,626 / 10,319 / 5,191 28,623 / 10,322 / 5,191

The three elements that move from mapped to approximated are exactly the all-refused state actions, which the report now describes as they are written: Activity ActuatorServoStates::CALIBRATE::update (a do action, "none of its 1 action is migrated, so it runs nothing"), Indexer::indexerStates::PWR_DOWN::startTimer (an entry action, the same), and PEAS::…::'Rigid Body and Segment Figure correction' (a do action, none of its 2 actions migrated). What is written for them does not change — the notation was already the action holding the flow and a comment per refused node — only the verdict, which previously called an action that runs nothing mapped. The model has no transition with a referenced effect and no unsupported-language opaque body, so nothing else moves.

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

… gate the PSSM suite

A transition whose effect writes no statements was written 'do action
effect;' before its 'then <target>', which the parser rejects; the effect
is now 'do action effect { }', a referenced effect 'do action : Def'
without ';', and the report says when an action is empty or runs
nothing. A transition's effect referring to a behavior owned elsewhere
was dropped silently and is now run.

TestPSSMSuiteMigration asserts the migrated OMG PSSM suite parses and
ratchets its report totals and validation errors; CI downloads the suite
and requires the gate.

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 September 21, 2026 03:58
devin-ai-integration[bot]

This comment was marked as resolved.

…is not run

A referenced entry, do, exit or effect that has no v2 declaration, or is
written as something no state runs, left the state or transition mapped
while only a comment was written for it; the owner is now approximated
with the reason, as it already was when the behavior's context could not
be bound.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…effect-body

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

# Conflicts:
#	internal/translate/migrate/carriers.go
#	internal/translate/migrate/states.go
@devin-ai-integration
devin-ai-integration Bot added this pull request to stack #496 September 21, 2026 15:53
@HuiJun
HuiJun merged commit 1f1edc9 into develop Sep 21, 2026
15 checks passed
@HuiJun
HuiJun deleted the fix/migrate-empty-effect-body branch September 21, 2026 19:53
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