Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes/unreleased/sequence-inherited-names.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- SysML v1 sequence migration avoids inherited occurrence member names when naming lifelines,
messages and parameters, keeping message ends and diagram layout references consistent.
3 changes: 1 addition & 2 deletions internal/translate/migrate/behavior.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,7 @@ func (m *migration) v2Refs(text, lang string, scope *sysmlv1.Element) (refs []re
// v2Spelled writes text, whose names are refs, as read inside scope: its
// library roots renamed and its owner's features qualified.
func (m *migration) v2Spelled(text string, refs []reference, scope *sysmlv1.Element) string {
visible, _ := m.visibleFrom(scope)
text = m.renamedRoots(text, refs, visible, nil)
text = m.renamedReferences(text, refs, scope)
refs, _ = exprRefs(text)
return m.qualifySelf(text, refs, scope)
}
Expand Down
87 changes: 83 additions & 4 deletions internal/translate/migrate/interaction.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package migrate

import (
"maps"
"slices"
"sort"
"strconv"
"strings"
"sync"

"github.com/Open-MBEE/OpenSysML/internal/translate/xmi/sysmlv1"
"github.com/Open-MBEE/OpenSysML/internal/workspace/libs"
)

const (
Expand All @@ -15,6 +18,40 @@ const (
refersToWord = "refers to "
)

var occurrenceNameCache sync.Map

func occurrenceNames() map[string]bool {
idx := libs.SharedBase()
cached, ok := occurrenceNameCache.Load(idx)
if !ok {
cached, _ = occurrenceNameCache.LoadOrStore(idx, sync.OnceValue(func() map[string]bool {
names, seen := map[string]bool{}, map[string]bool{}
queue := []string{"Occurrences::Occurrence"}
for len(queue) > 0 {
t := queue[0]
queue = queue[1:]
if seen[t] {
continue
}
seen[t] = true
for _, member := range idx.LookupDirectChildrenFrom(t, "") {
if member.Name != "" {
names[member.Name] = true
}
}
queue = append(queue, librarySupers(idx, t)...)
}
return names
}))
}
return cached.(func() map[string]bool)()
}

// inheritedOccurrenceNames returns a mutable copy of the cached inherited occurrence names.
func inheritedOccurrenceNames() map[string]bool {
return maps.Clone(occurrenceNames())
}

// scenario is an interaction resolved to the steps its v2 form performs in
// occurrence order: signal sends, operation calls, replies and combined fragments.
type scenario struct {
Expand Down Expand Up @@ -171,7 +208,7 @@ func (m *migration) scenario(e *sysmlv1.Element, self string) (*scenario, string
m: m, e: e, context: context, self: self,
order: map[*sysmlv1.Element]int{},
lines: map[*sysmlv1.Element]*lifelineRef{},
used: map[string]bool{},
used: inheritedOccurrenceNames(),
placed: map[*sysmlv1.Element]bool{},
unwritten: map[*sysmlv1.Element]string{},
uses: map[*sysmlv1.Element]*scenarioStep{},
Expand Down Expand Up @@ -322,6 +359,7 @@ func (s *scenario) use(f *sysmlv1.Element, body *[]*scenarioStep) (*scenarioStep
base = n
}
step.base = freshIn(s.used, base)
s.m.names[f] = step.base
step.name = writeName(step.base)
s.returnValue(step)
s.uses[f] = step
Expand Down Expand Up @@ -546,6 +584,10 @@ func (s *scenario) lifeline(line *sysmlv1.Element) (*lifelineRef, string) {
return nil, standsFor + describe(rep) + ", a " + rep.Type + " rather than a part or parameter"
}
ref.name = freshIn(s.used, name)
s.m.names[line] = ref.name
if rep.Parent == s.e {
s.m.names[rep] = ref.name
}
ref.from = writeName(ref.name)
s.lines[line] = ref
return ref, ""
Expand Down Expand Up @@ -863,7 +905,7 @@ func (s *scenario) pairArguments(args, targets []*sysmlv1.Element) []*sysmlv1.El
continue
}
for _, t := range targets {
if s.m.nameOf(t) == arg.Name {
if t.Name == arg.Name {
paired[i], named[t] = t, true
break
}
Expand Down Expand Up @@ -994,6 +1036,17 @@ func (s *scenario) inaccessible(expr string, line *lifelineRef) string {
e, _ = s.m.memberNamed(e, r.steps[n].name, chainKind(false))
n++
}
if e != nil && e.Parent == s.e {
if _, ok := s.m.sequenceParams[e]; ok {
continue
}
for _, ref := range s.lines {
if ref.rep == e {
e = nil
break
}
}
}
if e != nil && isFeature(e) {
return r.text(n)
}
Expand Down Expand Up @@ -1021,6 +1074,22 @@ func (s *scenario) parameterNamed(name string) bool {
return false
}

// sequenceFeature reports whether an interaction feature has a scenario declaration.
func (m *migration) sequenceFeature(e *sysmlv1.Element) bool {
if e.Parent == nil || e.Parent.Type != "Interaction" {
return false
}
if _, ok := m.sequenceParams[e]; ok {
return true
}
for _, line := range e.Parent.Owned("lifeline") {
if _, named := m.names[line]; named && m.model.Ref(line, "represents") == e {
return true
}
}
return false
}

// lifelineNamed finds the lifeline usage of the given name.
func (s *scenario) lifelineNamed(name string) *lifelineRef {
for _, ref := range s.lines {
Expand All @@ -1038,6 +1107,7 @@ func (s *scenario) stepName(msg *sysmlv1.Element, fallback string) (base, writte
name = lowerFirst(fallback)
}
base = freshIn(s.used, name)
s.m.names[msg] = base
return base, writeName(base)
}

Expand Down Expand Up @@ -1278,6 +1348,9 @@ func (s *scenario) writeSequence() {
ref := s.lines[line]
s.writeLifeline(ref)
s.m.names[line] = ref.name
if ref.rep.Parent == s.e {
s.m.names[ref.rep] = ref.name
}
note := "written as the " + strings.TrimSpace(ref.prefix+ref.kw) + " usage " + ref.name + " holding its " + plural(len(ref.events), "event occurrence")
switch {
case ref.bound != "":
Expand Down Expand Up @@ -1544,9 +1617,10 @@ func sequenceParameters(e *sysmlv1.Element) []*sysmlv1.Element {
return in
}

// declareParameters names the attributes the interaction's in parameters are written
// as, before the lifelines take their names; a parameter a lifeline stands for is that usage.
// declareParameters allocates all input names before writing defaults that may read later parameters.
// A parameter represented by a lifeline is declared by that usage instead.
func (s *scenario) declareParameters() {
var parameters []*sysmlv1.Element
for _, p := range sequenceParameters(s.e) {
represented := false
for _, ref := range s.lines {
Expand All @@ -1564,6 +1638,11 @@ func (s *scenario) declareParameters() {
}
name := freshIn(s.used, s.m.nameOf(p))
s.m.sequenceParams[p] = name
s.m.names[p] = name
parameters = append(parameters, p)
}
for _, p := range parameters {
name := s.m.sequenceParams[p]
line := "attribute " + writeName(name)
if mult, _ := s.m.multiplicity(p); mult != "" {
line += mult
Expand Down
30 changes: 30 additions & 0 deletions internal/translate/migrate/interaction_names_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package migrate

import "testing"

func TestInheritedOccurrenceNames(t *testing.T) {
names := inheritedOccurrenceNames()
for _, name := range []string{"self", "this", "portionOfLife", "suboccurrences", "spaceTimeEnclosedOccurrences"} {
if !names[name] {
t.Errorf("missing inherited occurrence member %q", name)
}
}
if names["that"] || names["earlierOccurrence"] || names["isEmpty"] {
t.Error("nested members and private imports are not inherited by Occurrence")
}
if got := freshIn(names, "self"); got != "self2" {
t.Errorf("fresh self = %q, want self2", got)
}
if got := freshIn(names, "self"); got != "self3" {
t.Errorf("second fresh self = %q, want self3", got)
}
if got := freshIn(names, ""); got != "" {
t.Errorf("anonymous name = %q, want empty", got)
}
if !inheritedOccurrenceNames()["self"] || inheritedOccurrenceNames()["self2"] {
t.Error("callers must receive independent name sets")
}
if !occurrenceNames()["self"] || occurrenceNames()["self2"] {
t.Error("fresh names must not mutate the shared set")
}
}
2 changes: 2 additions & 0 deletions internal/translate/migrate/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,8 @@ func (m *migration) add(e *sysmlv1.Element, v Verdict, target, note string) {
}
if src, clash := m.libraryClash[e]; clash {
renamed = writtenAs + n + " since a root package named " + src + " would be hidden by the standard library's " + src
} else if e.Parent != nil && e.Parent.Type == "Interaction" && occurrenceNames()[e.Name] {
renamed = writtenAs + n + " since an occurrence inherits a member named " + e.Name
} else {
renamed = writtenAs + n + " since a sibling is also named " + e.Name
}
Expand Down
71 changes: 52 additions & 19 deletions internal/translate/migrate/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,7 @@ func (m *migration) opaqueValue(v, scope *sysmlv1.Element, want wanted) (expr st
if problem := m.invisible(refs, scope); problem != "" {
return "", false, "opaque expression " + problem + langNote(lang)
}
visible, _ := m.visibleFrom(scope)
body = m.renamedRoots(body, refs, visible, nil)
body = m.renamedReferences(body, refs, scope)
if m.selfContext(scope) != nil {
// Inside a def the classifier's features read through its context parameter.
refs, _ = exprRefs(body)
Expand Down Expand Up @@ -406,6 +405,8 @@ type reference struct {
type step struct {
name string
chain bool
start int
len int
}

// text writes the reference's first n steps as the expression spells them.
Expand Down Expand Up @@ -453,6 +454,9 @@ func exprRefs(text string) (refs []reference, ok bool) {
}
for i := range c.refs {
c.refs[i].start -= len(exprProbePrefix)
for j := range c.refs[i].steps {
c.refs[i].steps[j].start -= len(exprProbePrefix)
}
}
return c.refs, true
}
Expand Down Expand Up @@ -775,7 +779,7 @@ func qualifiedRef(q *ast.QualifiedName) (reference, bool) {
}
r := reference{global: q.Global, start: q.Parts[0].Span.Offset, firstLen: q.Parts[0].Span.Len}
for _, p := range q.Parts {
r.steps = append(r.steps, step{name: p.Text})
r.steps = append(r.steps, step{name: p.Text, start: p.Span.Offset, len: p.Span.Len})
}
return r, true
}
Expand All @@ -797,7 +801,7 @@ func chainRef(e *ast.FeatureChainExpr) (reference, bool) {
return reference{}, false
}
for i, p := range e.Member.Parts {
r.steps = append(r.steps, step{name: p.Text, chain: i == 0})
r.steps = append(r.steps, step{name: p.Text, chain: i == 0, start: p.Span.Offset, len: p.Span.Len})
}
return r, true
}
Expand Down Expand Up @@ -839,7 +843,7 @@ func (m *migration) invisible(refs []reference, scope *sysmlv1.Element) string {
// fails when a source child of the failing element claims the name but is
// itself unwritten — the path is then the user's, not the library's.
func (m *migration) viaRenamedRoot(r reference, visible, hidden map[string]*sysmlv1.Element) (root *sysmlv1.Element, missing string) {
if r.local != "" || len(r.steps) == 0 || r.steps[0].chain {
if len(r.steps) == 0 || r.steps[0].chain {
return nil, ""
}
name := r.steps[0].name
Expand All @@ -855,7 +859,11 @@ func (m *migration) viaRenamedRoot(r reference, visible, hidden map[string]*sysm
}
e := ce
for i, s := range r.steps[1:] {
next, private := m.memberNamed(e, s.name, chainKind(s.chain))
kind := chainKind(s.chain)
if r.local != "" && i+1 == r.typed {
kind = memberFeature
}
next, private := m.memberNamed(e, s.name, kind)
if next == nil && private != nil && hidden != nil {
next = private
}
Expand All @@ -872,26 +880,39 @@ func (m *migration) viaRenamedRoot(r reference, visible, hidden map[string]*sysm
return ce, ""
}

// renamedRoots rewrites, in text, each reference whose first step spells the
// source name of a root avoidLibraryRoots renamed, to the name it is written
// as. A nearer element answering to the name keeps the spelling, and so does
// a reference that resolves only through the library package the root hides.
func (m *migration) renamedRoots(text string, refs []reference, visible, hidden map[string]*sysmlv1.Element) string {
// renamedReferences spells renamed roots and interaction members from their source references.
func (m *migration) renamedReferences(text string, refs []reference, scope *sysmlv1.Element) string {
visible, _ := m.visibleFrom(scope)
type replacement struct {
start, end int
name string
}
var reps []replacement
byStart := map[int]replacement{}
for _, r := range refs {
if r.local != "" || len(r.steps) == 0 || r.steps[0].chain || r.firstLen == 0 {
if r.local != "" && r.typed == 0 {
continue
}
if ce, _ := m.viaRenamedRoot(r, visible, hidden); ce != nil {
reps = append(reps, replacement{r.start, r.start + r.firstLen, writeName(m.names[ce])})
if root, _ := m.viaRenamedRoot(r, visible, nil); root != nil && r.firstLen != 0 {
byStart[r.start] = replacement{r.start, r.start + r.firstLen, writeName(m.nameOf(root))}
}
for i, part := range r.steps {
prefix := r
prefix.steps = r.steps[:i+1]
e, _, missing := m.resolve(prefix, visible, nil)
if missing != "" || e == nil || e.Parent == nil || e.Parent.Type != "Interaction" || part.len == 0 {
continue
}
if name := m.nameOf(e); name != part.name {
byStart[part.start] = replacement{part.start, part.start + part.len, writeName(name)}
}
}
}
sort.Slice(reps, func(i, j int) bool { return reps[i].start > reps[j].start })
for _, rp := range reps {
var replacements []replacement
for _, rp := range byStart {
replacements = append(replacements, rp)
}
sort.Slice(replacements, func(i, j int) bool { return replacements[i].start > replacements[j].start })
for _, rp := range replacements {
text = text[:rp.start] + rp.name + text[rp.end:]
}
return text
Expand Down Expand Up @@ -1068,7 +1089,7 @@ func (m *migration) membersOf(e *sysmlv1.Element, kind memberKind) (visible, hid
seen[t] = true
for _, c := range t.Children {
n := m.nameOf(c)
if n == "" || !m.written(c) || (features && c.Type != "Property" && c.Type != "Port") {
if n == "" || (!m.sequenceFeature(c) && !m.written(c)) || (features && c.Type != "Property" && c.Type != "Port") {
continue
}
if m.hiddenFromHeirs(c) {
Expand All @@ -1081,6 +1102,15 @@ func (m *migration) membersOf(e *sysmlv1.Element, kind memberKind) (visible, hid
visible[n] = c
}
}
for _, c := range t.Children {
if t.Type == "Interaction" && c.Name != "" && (m.sequenceFeature(c) || m.written(c)) && (!features || c.Type == "Property" || c.Type == "Port") {
if m.hiddenFromHeirs(c) {
hidden[c.Name] = c
} else {
visible[c.Name] = c
}
}
}
for _, g := range t.Owned("generalization") {
walk(m.model.Ref(g, "general"))
}
Expand Down Expand Up @@ -1151,7 +1181,10 @@ func (v *visibility) members(e *sysmlv1.Element, inherited bool) {

func (v *visibility) member(c *sysmlv1.Element, inherited bool) {
n := v.m.nameOf(c)
if n == "" || !v.m.written(c) {
if c.Parent != nil && c.Parent.Type == "Interaction" && c.Name != "" {
n = c.Name
}
if n == "" || (!v.m.sequenceFeature(c) && !v.m.written(c)) {
return
}
if inherited && v.m.hiddenFromHeirs(c) {
Expand Down
Loading
Loading