Skip to content
Merged
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
55 changes: 35 additions & 20 deletions internal/check/passes/typecheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/doc/queryexec/named_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
172 changes: 102 additions & 70 deletions internal/doc/queryexec/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 6 additions & 3 deletions internal/exec/smt/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions internal/frontend/grpc/query_operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion internal/ir/docplan/markup/asciidoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions internal/ir/view/case_mixed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
4 changes: 2 additions & 2 deletions internal/ir/view/dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
Loading
Loading