diff --git a/changes/unreleased/conform-by-symbol.fixed.md b/changes/unreleased/conform-by-symbol.fixed.md new file mode 100644 index 0000000000..a629f59a2e --- /dev/null +++ b/changes/unreleased/conform-by-symbol.fixed.md @@ -0,0 +1,8 @@ +- A conformance check against a named library type — the ones every redefinition's derived + `isVariable`/`isConstant` ask through `Usage::mayTimeVary` (`Occurrences::Occurrence`, + `Links::SelfLink`, `Occurrences::HappensLink`, `Actions::Action`) — compares the candidate and + its memoized supertypes to the one element registered under the name instead of building the + qualified name of each supertype on every check, which the 0.11.0 performance record found + costing the connector-dense `ImportValues` benchmarks 29% more allocations (135.5 k → 175.2 k + per load) and a synthetic document's analysis 10% more. A name no or several elements carry + is still compared by name. diff --git a/internal/semantic/semantics/conforms_by_name_allocs_test.go b/internal/semantic/semantics/conforms_by_name_allocs_test.go new file mode 100644 index 0000000000..124750b9bd --- /dev/null +++ b/internal/semantic/semantics/conforms_by_name_allocs_test.go @@ -0,0 +1,38 @@ +package semantics + +import ( + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" +) + +// A conformance check against a named type compares the candidate's and its +// memoized supertypes' owner chains to the name: no qualified name is built. +func TestConformsByNameDoesNotBuildNames(t *testing.T) { + idx := symbols.NewIndex() + addTestDoc(t, idx, "d.sysml", `package T { + part def Base; + part def Mid :> Base; + part def Leaf :> Mid; + part def Other; + }`) + idx.ExpandWildcardImports() + r := resolve.New(idx) + m := NewModel(r) + r.SetModel(m) + leaf := idx.LookupQualified("T::Leaf")[0] + + cases := []struct { + fqn string + want bool + }{{"T::Base", true}, {"T::Mid", true}, {"T::Leaf", true}, {"T::Other", false}, {"T::Absent", false}, {"Base", false}} + for _, c := range cases { + if got := m.conformsByName(leaf, c.fqn); got != c.want { + t.Fatalf("conformsByName(Leaf, %s) = %v, want %v", c.fqn, got, c.want) + } + if allocs := testing.AllocsPerRun(20, func() { m.conformsByName(leaf, c.fqn) }); allocs != 0 { + t.Errorf("a repeated conformsByName(Leaf, %s) allocates %.0f objects, want 0", c.fqn, allocs) + } + } +} diff --git a/internal/semantic/semantics/filter.go b/internal/semantic/semantics/filter.go index c2154cb3f8..91cd3885bb 100644 --- a/internal/semantic/semantics/filter.go +++ b/internal/semantic/semantics/filter.go @@ -985,11 +985,28 @@ func (m *Model) conformsByName(sym *symbols.Symbol, typeFQN string) bool { if sym == nil || typeFQN == "" { return false } - if m.fqnOf(sym) == typeFQN { + if m.resolver == nil || m.resolver.Index() == nil { + if sym.Name == typeFQN { + return true + } + for _, sup := range m.AllSupertypes(sym) { + if sup.Name == typeFQN { + return true + } + } + return false + } + // The names are compared segment by segment against the owner chain, so + // no qualified name is built for the candidate or any supertype. + parent, leaf := "", typeFQN + if i := strings.LastIndex(typeFQN, "::"); i >= 0 { + parent, leaf = typeFQN[:i], typeFQN[i+2:] + } + if symbols.FQNIs(sym, parent, leaf) { return true } for _, sup := range m.AllSupertypes(sym) { - if m.fqnOf(sup) == typeFQN { + if symbols.FQNIs(sup, parent, leaf) { return true } }