diff --git a/internal/check/passes/typecheck.go b/internal/check/passes/typecheck.go index ee1ecd0732..3311fbc984 100644 --- a/internal/check/passes/typecheck.go +++ b/internal/check/passes/typecheck.go @@ -387,6 +387,32 @@ func (tc *typeChecker) checkRelationships(scope *symbols.Scope, rels []*ast.Rela // checkTypeTarget checks one relationship target against the kind rules for the // declaration carrying it. func (tc *typeChecker) checkTypeTarget(scope *symbols.Scope, target ast.Node, relKind ast.RelationshipKind, decl declKind) { + targetSym, ok := tc.typeTargetSymbol(scope, target, relKind, decl) + if !ok { + return + } + if relKind == ast.RelRedefines || relKind == ast.RelSubsets { + if tc.checkNearestDeclaredUsageTyping(targetSym, decl) { + return + } + } + if relKind == ast.RelTyping && decl.useKind == ast.UsageEnumeration && + tc.owningEnumerationConformsTo(scope, targetSym) { + return + } + if relKind == ast.RelSpecializes && w11aFamilyRuleFires(decl, targetSym) { + return // a supertype of the wrong classifier family is the family rules' finding + } + if relKind == ast.RelTyping && targetSym.Kind == symbols.SymbolKerMLType { + tc.checkKerMLTyping(decl, targetSym) + return + } + tc.reportTargetKind(target, relKind, decl, targetSym) +} + +// typeTargetSymbol resolves a target to the symbol the kind rules judge (an alias +// to its target where kind matters), or false when unresolved or a chain no rule judges. +func (tc *typeChecker) typeTargetSymbol(scope *symbols.Scope, target ast.Node, relKind ast.RelationshipKind, decl declKind) (*symbols.Symbol, bool) { // Unwrap FeatureReference if needed targetNode := target if fr, ok := targetNode.(*ast.FeatureReference); ok { @@ -400,43 +426,32 @@ func (tc *typeChecker) checkTypeTarget(scope *symbols.Scope, target ast.Node, re tc.checkChainSegments(scope, targetNode) tc.checkChainReferenceKind(scope, targetNode, relKind, decl) if relKind != ast.RelTyping { - return + return nil, false } // A feature chain types a feature as a qualified name does (KerML 8.2.3 // OwnedFeatureTyping), so the feature it ends in meets the same kind rules. sym, ok = tc.resolver.ResolveTarget(scope, targetNode) } if !ok || sym == nil { - return // unresolved: name-resolution tier owns this + return nil, false // unresolved: name-resolution tier owns this } // Resolve aliases to their underlying types for the relationships whose // check depends on the target's kind: a typing and a generalization. - targetSym := sym aliasMatters := relKind == ast.RelSpecializes || relKind == ast.RelSubsets || relKind == ast.RelRedefines || relKind == ast.RelTyping if aliasMatters && sym.Kind == symbols.SymbolAlias { if resolved, ok := tc.resolver.ResolveAliasTarget(sym); ok && resolved != nil { - targetSym = resolved - } - } - if relKind == ast.RelRedefines || relKind == ast.RelSubsets { - if tc.checkNearestDeclaredUsageTyping(targetSym, decl) { - return + return resolved, true } } - if relKind == ast.RelTyping && decl.useKind == ast.UsageEnumeration && - tc.owningEnumerationConformsTo(scope, targetSym) { - return - } - if relKind == ast.RelSpecializes && w11aFamilyRuleFires(decl, targetSym) { - return // a supertype of the wrong classifier family is the family rules' finding - } - if relKind == ast.RelTyping && targetSym.Kind == symbols.SymbolKerMLType { - tc.checkKerMLTyping(decl, targetSym) - return - } + return sym, true +} + +// reportTargetKind reports a target whose kind the declaration's relationship +// does not admit, worded per kind for a typing as the pilot words it. +func (tc *typeChecker) reportTargetKind(target ast.Node, relKind ast.RelationshipKind, decl declKind, targetSym *symbols.Symbol) { kind := targetSym.Kind if relKind.ReferenceSubsets() || relKind == ast.RelSubsets { kind = referentKind(targetSym) diff --git a/internal/doc/queryexec/named_test.go b/internal/doc/queryexec/named_test.go index 1f714934f6..9acbc43e61 100644 --- a/internal/doc/queryexec/named_test.go +++ b/internal/doc/queryexec/named_test.go @@ -140,7 +140,7 @@ func TestExecuteNamedReadsQuotedSegments(t *testing.T) { _, err := fixture.execute(t, "ByName", Bindings{"qualifiedName": {StringValue(name)}}, Options{}) unknown := executionError(t, err, ErrorUnknownElement) if unknown.Actual != name || !strings.Contains(err.Error(), want) { - t.Fatalf("Named(%s) = %v, want it to contain %q", name, err, want) + t.Fatalf("Named(%s) = %v naming %q, want it to name %q and contain %q", name, err, unknown.Actual, name, want) } } // Escapes are read as the parser reads them; a quoted `::` is one segment, so diff --git a/internal/doc/queryexec/operations.go b/internal/doc/queryexec/operations.go index 726c0bfebe..0a6e3d189a 100644 --- a/internal/doc/queryexec/operations.go +++ b/internal/doc/queryexec/operations.go @@ -606,100 +606,132 @@ func (e *executor) evaluateProject(expression queryplan.Expression) (sequence, e if err != nil { return sequence{}, err } - var properties []string - if hasArgument(expression, "properties") { - properties, err = e.stringsArgument(expression, "properties") - if err != nil { + p, err := e.newProjection(expression) + if err != nil { + return sequence{}, err + } + total := len(p.properties) + len(p.computed) + result := sequence{ + values: append([]Value(nil), source.values...), + columns: make([]Column, total), + cells: make([][]Cell, len(source.values)), + depths: append([]int64(nil), source.depths...), + } + for i, property := range p.properties { + result.columns[i] = Column{name: property, origin: expression.Origin()} + } + for i, column := range p.computed { + result.columns[len(p.properties)+i] = Column{name: column.name, origin: column.origin.Origin()} + } + for row, value := range source.values { + if result.cells[row], err = p.cells(value); err != nil { return sequence{}, err } } - var computed []computedColumn - if columnsValue, ok := argumentValue(expression, "columns"); ok { - computed, err = e.computedColumns(expression, columnsValue) - if err != nil { + if len(source.values) > 0 { + if err := p.checkProperties(); err != nil { return sequence{}, err } } - total := len(properties) + len(computed) + return result, nil +} + +// projection is one project operation's columns and what reading its rows +// learnt of them: which properties any row carried and which rows lacked one. +type projection struct { + e *executor + expression queryplan.Expression + properties []string + computed []computedColumn + tolerate bool + tracker *propertyTracker + known []bool + missing [][]*Error +} + +// newProjection reads the operation's properties and computed columns, at +// least one in all and each named once. +func (e *executor) newProjection(expression queryplan.Expression) (*projection, error) { + p := &projection{e: e, expression: expression} + var err error + if hasArgument(expression, "properties") { + if p.properties, err = e.stringsArgument(expression, "properties"); err != nil { + return nil, err + } + } + if columnsValue, ok := argumentValue(expression, "columns"); ok { + if p.computed, err = e.computedColumns(expression, columnsValue); err != nil { + return nil, err + } + } + total := len(p.properties) + len(p.computed) if total == 0 { - return sequence{}, e.invalidArgument(expression, "properties", "empty") + return nil, e.invalidArgument(expression, "properties", "empty") } - tolerate, err := e.tolerateArgument(expression) - if err != nil { - return sequence{}, err + if p.tolerate, err = e.tolerateArgument(expression); err != nil { + return nil, err } seen := make(map[string]bool, total) - for _, property := range properties { + for _, property := range p.properties { seen[property] = true } - for _, column := range computed { + for _, column := range p.computed { if seen[column.name] { - return sequence{}, e.invalidArgument(expression, "columns", column.name) + return nil, e.invalidArgument(expression, "columns", column.name) } seen[column.name] = true } - result := sequence{ - values: append([]Value(nil), source.values...), - columns: make([]Column, total), - cells: make([][]Cell, len(source.values)), - depths: append([]int64(nil), source.depths...), - } - known := make([]bool, len(properties)) - for i, property := range properties { - result.columns[i] = Column{name: property, origin: expression.Origin()} - } - for i, column := range computed { - result.columns[len(properties)+i] = Column{name: column.name, origin: column.origin.Origin()} - } - tracker := newPropertyTracker() - missing := make([][]*Error, len(properties)) - for row, value := range source.values { - result.cells[row] = make([]Cell, total) - for column, property := range properties { - values, present, valueErr := e.propertyValues(value, property) - if valueErr != nil { - return sequence{}, e.unevaluable(expression, property, value, valueErr) - } - if !present && !tolerate { - if m := e.missingFeature(expression, property, value); m != nil { - missing[column] = append(missing[column], m) - } - } - known[column] = known[column] || present - result.cells[row][column] = Cell{ - values: values, - origin: value.Origin(), - } + p.tracker = newPropertyTracker() + p.known = make([]bool, len(p.properties)) + p.missing = make([][]*Error, len(p.properties)) + return p, nil +} + +// cells reads one row's columns, noting the properties it carries and lacks. +func (p *projection) cells(value Value) ([]Cell, error) { + cells := make([]Cell, len(p.properties)+len(p.computed)) + for column, property := range p.properties { + values, present, err := p.e.propertyValues(value, property) + if err != nil { + return nil, p.e.unevaluable(p.expression, property, value, err) } - for i, column := range computed { - values, cellErr := e.evaluateColumnCell(column, value, tracker) - if cellErr != nil { - return sequence{}, cellErr - } - result.cells[row][len(properties)+i] = Cell{ - values: values, - origin: value.Origin(), + if !present && !p.tolerate { + if m := p.e.missingFeature(p.expression, property, value); m != nil { + p.missing[column] = append(p.missing[column], m) } } + p.known[column] = p.known[column] || present + cells[column] = Cell{values: values, origin: value.Origin()} } - if len(source.values) > 0 { - for i, property := range properties { - if !known[i] { - return sequence{}, e.unknownProperty(expression, property) - } + for i, column := range p.computed { + values, err := p.e.evaluateColumnCell(column, value, p.tracker) + if err != nil { + return nil, err } - // A property some rows carry and an element row lacks is a path error - // about that element, unless tolerated; one no row carries is unknown. - for _, column := range missing { - if err := joinMissing(column); err != nil { - return sequence{}, err - } + cells[len(p.properties)+i] = Cell{values: values, origin: value.Origin()} + } + return cells, nil +} + +// checkProperties reports, once every row is read, a property no row carried +// as unknown, then the rows that lacked one others carry. +func (p *projection) checkProperties() error { + for i, property := range p.properties { + if !p.known[i] { + return p.e.unknownProperty(p.expression, property) } - if property, missing := tracker.missing(); missing { - return sequence{}, e.unknownProperty(expression, property) + } + // A property some rows carry and an element row lacks is a path error + // about that element, unless tolerated; one no row carries is unknown. + for _, column := range p.missing { + if err := joinMissing(column); err != nil { + return err } } - return result, nil + if property, missing := p.tracker.missing(); missing { + return p.e.unknownProperty(p.expression, property) + } + return nil } // propertyValues reads a property of a row: of the session for an object row, diff --git a/internal/exec/smt/encode.go b/internal/exec/smt/encode.go index 4b05897aba..c467a24845 100644 --- a/internal/exec/smt/encode.go +++ b/internal/exec/smt/encode.go @@ -239,6 +239,9 @@ func (e *Encoding) collectFeatures() error { return nil } +// attributeConstruct names an attribute in the construct an UnsupportedError reports. +const attributeConstruct = "attribute " + // collectAttribute declares the variable of an attribute the performance holds, // free or valued, and records it as an input; declared gains the variable. func (e *Encoding) collectAttribute(attr lower.Attribute, declared map[string]bool) error { @@ -254,16 +257,16 @@ func (e *Encoding) collectAttribute(attr lower.Attribute, declared map[string]bo written = attr.Symbol.Name } if fp := lower.ExpressionFootprint(graph.Resolver(), graph.Semantics(), scope, attr.Value); attr.Binding && (len(fp.Reads) > 0 || fp.Dynamic) { - return &UnsupportedError{Node: nodeLabel(graph.Initial), Construct: "attribute " + attr.Name, + return &UnsupportedError{Node: nodeLabel(graph.Initial), Construct: attributeConstruct + attr.Name, Reason: "a feature valued by an expression over other features is computed as it is read; the states carry only values written"} } free, released := e.frees(attr) if free { - if _, err := e.translator.Variable(written, scope, "attribute "+attr.Name); err != nil { + if _, err := e.translator.Variable(written, scope, attributeConstruct+attr.Name); err != nil { return noDomain(attr, err) } } - v, err := e.variable(written, scope, "attribute "+attr.Name, nodeLabel(graph.Initial)) + v, err := e.variable(written, scope, attributeConstruct+attr.Name, nodeLabel(graph.Initial)) if err != nil { return err } diff --git a/internal/frontend/grpc/query_operators_test.go b/internal/frontend/grpc/query_operators_test.go index 3f39015dc5..488e306c26 100644 --- a/internal/frontend/grpc/query_operators_test.go +++ b/internal/frontend/grpc/query_operators_test.go @@ -167,9 +167,9 @@ func TestQueryInstanceOfSelectsByMetaclassConformance(t *testing.T) { // TestQueryInstanceOfAnyOfAListAndInverse verifies a list under instanceOf // matches an instance of any listed metaclass, and inverse negates the verdict. func TestQueryInstanceOfAnyOfAListAndInverse(t *testing.T) { - any := mustRunQuery(t, &pb.Query{Where: primitive(QueryPropType, opInstanceOf, false, "Definition", "AttributeUsage")}) - if want := []string{"Demo::Vehicle", "Demo::Vehicle::mass", "Demo::Wheel", "Demo::vehicle::vin"}; !slices.Equal(any, want) { - t.Errorf("@type instanceOf [Definition, AttributeUsage] = %v, want %v", any, want) + listed := mustRunQuery(t, &pb.Query{Where: primitive(QueryPropType, opInstanceOf, false, "Definition", "AttributeUsage")}) + if want := []string{"Demo::Vehicle", "Demo::Vehicle::mass", "Demo::Wheel", "Demo::vehicle::vin"}; !slices.Equal(listed, want) { + t.Errorf("@type instanceOf [Definition, AttributeUsage] = %v, want %v", listed, want) } notUsage := mustRunQuery(t, &pb.Query{Where: primitive(QueryPropType, opInstanceOf, true, "Usage")}) if want := []string{"Demo", "Demo::Vehicle", "Demo::Wheel", "Demo::vehicle::wheels::@0"}; !slices.Equal(notUsage, want) { diff --git a/internal/ir/docplan/markup/asciidoc.go b/internal/ir/docplan/markup/asciidoc.go index a048a2cf93..6443a7965f 100644 --- a/internal/ir/docplan/markup/asciidoc.go +++ b/internal/ir/docplan/markup/asciidoc.go @@ -132,7 +132,7 @@ func decodeAsciiDoc(body string, output []byte) ([]Block, error) { // flattenAsciiDoc lays the converter's tree out in body order: a section is // its heading followed by its blocks, subsections among them. -func flattenAsciiDoc(flat []asciiDocBlock, nodes []asciiDocBlock) []asciiDocBlock { +func flattenAsciiDoc(flat, nodes []asciiDocBlock) []asciiDocBlock { for _, node := range nodes { if node.Kind != "section" { flat = append(flat, node) diff --git a/internal/ir/view/case_mixed_test.go b/internal/ir/view/case_mixed_test.go index 6b58882e2a..8d13ae66dd 100644 --- a/internal/ir/view/case_mixed_test.go +++ b/internal/ir/view/case_mixed_test.go @@ -1201,3 +1201,16 @@ func mapNodeNames(nodes map[string][]*Node) []string { sort.Strings(names) return names } + +// TestEdgeKindNamesEveryKind checks every edge kind has a name of its own, so a +// kind added without one cannot render as a bare "edge". +func TestEdgeKindNamesEveryKind(t *testing.T) { + for kind := EdgeConnection; kind < edgeKindCount; kind++ { + if got := kind.String(); got == "edge" || got == "" { + t.Errorf("EdgeKind(%d).String() = %q, want a name of its own", int(kind), got) + } + } + if got := EdgeKind(edgeKindCount).String(); got != "edge" { + t.Errorf("EdgeKind(edgeKindCount).String() = %q, want %q", got, "edge") + } +} diff --git a/internal/ir/view/dot.go b/internal/ir/view/dot.go index 35a5e96cb7..1a2569fc8c 100644 --- a/internal/ir/view/dot.go +++ b/internal/ir/view/dot.go @@ -2139,7 +2139,7 @@ func (w *dotWriter) dotEdgeAttributes(edge Edge) []string { case EdgeSuccessionFlow: attrs = append(attrs, dotStyleDashed) case EdgeRequire, EdgeAssume, EdgeAssert, EdgeFrame, EdgeEvent: - attrs = append(attrs, "arrowhead=vee") + attrs = append(attrs, dotArrowheadVee) case EdgePortion: // Graphviz has no three-quarter circle: a filled circle stands at the occurrence end. attrs = append(attrs, dotDirBack, "arrowtail=dot") @@ -2149,7 +2149,7 @@ func (w *dotWriter) dotEdgeAttributes(edge Edge) []string { case EdgeUnownedMembership: attrs = append(attrs, dotDirBack, "arrowtail=odot") case EdgeExpose: - attrs = append(attrs, "arrowhead=vee", dotStyleDashed) + attrs = append(attrs, dotArrowheadVee, dotStyleDashed) } attrs = append(attrs, dotStyleAttributes(edge.Style, false)...) if len(edge.Route) > 1 { diff --git a/internal/ir/view/general.go b/internal/ir/view/general.go index 3cc7ebbe36..0137d4d0b0 100644 --- a/internal/ir/view/general.go +++ b/internal/ir/view/general.go @@ -595,6 +595,12 @@ func (g *generalGraph) packageEdges(pkg *symbols.Symbol) { if pkg.Scope == nil { return } + g.aliasEdges(pkg, node) + g.importEdges(pkg, node) +} + +// aliasEdges draws the package's aliases of drawn elements as unowned memberships. +func (g *generalGraph) aliasEdges(pkg *symbols.Symbol, node *Node) { for _, member := range pkg.Scope.Members() { if member.Kind != symbols.SymbolAlias { continue @@ -610,6 +616,10 @@ func (g *generalGraph) packageEdges(pkg *symbols.Symbol) { g.add(Edge{From: node.ID, To: to.ID, Kind: EdgeUnownedMembership, ToLabel: member.Name, Origin: symbolOrigin(member)}) } } +} + +// importEdges draws the package's imports of drawn packages, or of members of them. +func (g *generalGraph) importEdges(pkg *symbols.Symbol, node *Node) { for _, imp := range pkg.Scope.Imports() { if imp == nil || imp.IsExpose || imp.Imported == nil { continue diff --git a/internal/ir/view/view.go b/internal/ir/view/view.go index aa4d6c78e4..3c0e268d8b 100644 --- a/internal/ir/view/view.go +++ b/internal/ir/view/view.go @@ -282,79 +282,52 @@ const ( EdgeUnownedMembership // EdgeExpose is a view's exposure of an element, from the view. EdgeExpose + // edgeKindCount is one past the last edge kind, sizing edgeKindNames. + edgeKindCount ) +// edgeKindNames names each edge kind the way the notation speaks of it. +var edgeKindNames = [edgeKindCount]string{ + EdgeConnection: "connection", + EdgeTransition: "transition", + EdgeSuccession: "succession", + EdgeFlow: "flow", + EdgeBinding: "binding", + EdgeSpecialization: "specialization", + EdgeTyping: "typing", + EdgeComposition: "composition", + EdgeReference: "reference", + EdgeContainment: "containment", + EdgeImport: "import", + EdgeSatisfy: "satisfy", + EdgeVerify: "verify", + EdgeDerive: "derive", + EdgeRefine: "refine", + EdgeAllocate: "allocate", + EdgeAssociation: "association", + EdgeInclude: "include", + EdgeAnchor: "anchor", + EdgeDependency: "dependency", + EdgeSubsetting: "subsetting", + EdgeRedefinition: "redefinition", + EdgePerform: "perform", + EdgeExhibit: "exhibit", + EdgeSuccessionFlow: "succession flow", + EdgeRequire: "require", + EdgeAssume: "assume", + EdgeAssert: "assert", + EdgeFrame: "frame", + EdgeEvent: "event", + EdgePortion: "portion", + EdgeReferenceSubsetting: "reference subsetting", + EdgeUnownedMembership: "unowned membership", + EdgeExpose: "expose", +} + // String names an edge kind the way the notation speaks of it. func (k EdgeKind) String() string { - switch k { - case EdgeConnection: - return "connection" - case EdgeTransition: - return "transition" - case EdgeSuccession: - return "succession" - case EdgeFlow: - return "flow" - case EdgeBinding: - return "binding" - case EdgeSpecialization: - return "specialization" - case EdgeTyping: - return "typing" - case EdgeComposition: - return "composition" - case EdgeReference: - return "reference" - case EdgeContainment: - return "containment" - case EdgeImport: - return "import" - case EdgeSatisfy: - return "satisfy" - case EdgeVerify: - return "verify" - case EdgeDerive: - return "derive" - case EdgeRefine: - return "refine" - case EdgeAllocate: - return "allocate" - case EdgeAssociation: - return "association" - case EdgeInclude: - return "include" - case EdgeAnchor: - return "anchor" - case EdgeDependency: - return "dependency" - case EdgeSubsetting: - return "subsetting" - case EdgeRedefinition: - return "redefinition" - case EdgePerform: - return "perform" - case EdgeExhibit: - return "exhibit" - case EdgeSuccessionFlow: - return "succession flow" - case EdgeRequire: - return "require" - case EdgeAssume: - return "assume" - case EdgeAssert: - return "assert" - case EdgeFrame: - return "frame" - case EdgeEvent: - return "event" - case EdgePortion: - return "portion" - case EdgeReferenceSubsetting: - return "reference subsetting" - case EdgeUnownedMembership: - return "unowned membership" - case EdgeExpose: - return "expose" + if k >= 0 && k < edgeKindCount && edgeKindNames[k] != "" { + return edgeKindNames[k] } return "edge" } diff --git a/internal/semantic/semantics/model.go b/internal/semantic/semantics/model.go index ff45463678..f3576cabd6 100644 --- a/internal/semantic/semantics/model.go +++ b/internal/semantic/semantics/model.go @@ -989,27 +989,7 @@ func (m *Model) FeatureTypeSetWithImplied(sym *symbols.Symbol) []*symbols.Symbol types = append(types, typ) } } - bases := m.ImplicitGenerals(sym) - // A membership's own base (a case's subject) can stand in for the kind's among the implied - // generals, so the base feature of the symbol's kind joins too (a subject is a part usage). - if _, isUsage := sym.Decl.(*ast.Usage); (isUsage || sym.Kind == symbols.SymbolPartUsage) && !m.isKerMLDoc(sym) && m.resolver != nil && m.resolver.Index() != nil { - fqns := []string{} - if fqn, ok := m.FeatureBaseFQN(sym); ok { - fqns = append(fqns, fqn) - } - for kind, fqn := range implicitUsageBases { - if fqn != lifeFQN && symbols.UsageSymbolKind(kind) == sym.Kind { - fqns = append(fqns, fqn) - } - } - for _, fqn := range fqns { - for _, base := range m.resolver.Index().LookupQualified(fqn) { - if base != nil && base != sym && !containsElement(bases, base) { - bases = append(bases, base) - } - } - } - } + bases := m.withUsageBases(sym, m.ImplicitGenerals(sym)) for _, general := range bases { if general == nil { continue @@ -1028,6 +1008,32 @@ func (m *Model) FeatureTypeSetWithImplied(sym *symbols.Symbol) []*symbols.Symbol return m.mostSpecificTypes(types) } +// withUsageBases adds a usage kind's base features to its implied generals, where a +// membership's own base (a case's subject) may stand in for the kind's (a part usage). +func (m *Model) withUsageBases(sym *symbols.Symbol, bases []*symbols.Symbol) []*symbols.Symbol { + _, isUsage := sym.Decl.(*ast.Usage) + if !(isUsage || sym.Kind == symbols.SymbolPartUsage) || m.isKerMLDoc(sym) || m.resolver == nil || m.resolver.Index() == nil { + return bases + } + fqns := []string{} + if fqn, ok := m.FeatureBaseFQN(sym); ok { + fqns = append(fqns, fqn) + } + for kind, fqn := range implicitUsageBases { + if fqn != lifeFQN && symbols.UsageSymbolKind(kind) == sym.Kind { + fqns = append(fqns, fqn) + } + } + for _, fqn := range fqns { + for _, base := range m.resolver.Index().LookupQualified(fqn) { + if base != nil && base != sym && !containsElement(bases, base) { + bases = append(bases, base) + } + } + } + return bases +} + // mostSpecificTypes drops every type another one in the list specializes. func (m *Model) mostSpecificTypes(types []*symbols.Symbol) []*symbols.Symbol { var out []*symbols.Symbol diff --git a/internal/workspace/libs/loader_copies_test.go b/internal/workspace/libs/loader_copies_test.go index 71e46902f4..45500cf450 100644 --- a/internal/workspace/libs/loader_copies_test.go +++ b/internal/workspace/libs/loader_copies_test.go @@ -16,8 +16,8 @@ import ( // either copy resolves D's supertype to the B declared beside it. func copiedLibrary(t *testing.T, dir string) { t.Helper() - for _, copy := range []string{"one", "two"} { - path := filepath.Join(dir, copy, "Lib.kerml") + for _, replica := range []string{"one", "two"} { + path := filepath.Join(dir, replica, "Lib.kerml") if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { t.Fatal(err) } @@ -81,9 +81,9 @@ func TestLoaderRestoresEachCopyFromItsOwnRecord(t *testing.T) { t.Errorf("%s: warm supertypes %q, cold %q", key, got, want) } } - for _, copy := range []string{"one", "two"} { - key := copy + "/Lib.kerml#Lib::D" - if want := copy + "/Lib.kerml#Lib::B"; coldSupers[key] != want { + for _, replica := range []string{"one", "two"} { + key := replica + "/Lib.kerml#Lib::D" + if want := replica + "/Lib.kerml#Lib::B"; coldSupers[key] != want { t.Errorf("%s: cold supertypes %q, want %q", key, coldSupers[key], want) } }