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
1 change: 1 addition & 0 deletions changes/unreleased/library-element-names.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **A converted model names the standard library elements it references.** A reference to a library element is its normative id, a hash no reader can turn back into a name. Turtle, the API's JSON and `Convert` now state each referenced library element under that id, with its metaclass, `qualifiedName` and `declaredName`, marked `isLibraryElement` (KerML `Element::isLibraryElement`). Every `@id` an API JSON output references is then an element of it. The library itself is still not exported, and reading a graph back checks each name against the bundled library (#731).
23 changes: 23 additions & 0 deletions docs/reference/rdf-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,29 @@ still wins over the normative id when a library element carries one; an
annotation restating the norm's own id declares nothing, so the id stays
normative and `sysx:declaredId` is not written.

**A converted model names the library elements it references.** A normative
id is a hash of a name, and cannot be turned back into one. So for each standard
library element the output references, the writer states the element under its
normative id, marked `sysml:isLibraryElement true` (KerML
`Element::isLibraryElement`, `Element_isLibraryElement` in the ontology), with:
- its `rdf:type` (`LibraryPackage` for a library package);
- `sysml:elementId`, `sysml:qualifiedName` and `sysml:declaredName`.

Nothing else is written about it: its owner, members and relationships stay in
the library, and the library itself is not exported. A library membership the
output references (the `importedMembership` of `import ScalarValues::Real;`) is
stated as a `sysml:OwningMembership` with its `sysml:memberElement`, marked the
same way.

So every `@id` an API JSON output references is an element of it. Such an
element has no owner in the graph, and the document's root namespace does not
take it (`LibraryReference`).

Reading a graph back, such an element is a reference into the bundled library,
not a declaration, and it is not written. A qualified name other than the one
the library gives its id is refused rather than trusted, and so is an id the
library does not have. Repository sync never creates, changes or deletes one.

**What is a version of a library file.** A document is a version of a bundled
library file when every one of its roots is a top-level package that file
declares, under the same qualified name, and all of one file. On the graph side
Expand Down
5 changes: 5 additions & 0 deletions internal/translate/export/api_json_import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ func countTable(t *testing.T, data []byte) map[string]int {
}
table := map[string]int{}
for _, object := range objects {
// A library element the export names (isLibraryElement) is a reference
// into the library, not an element of the model.
if object["isLibraryElement"] == true {
continue
}
if typ, ok := object["@type"].(string); ok {
table[typ]++
}
Expand Down
68 changes: 67 additions & 1 deletion internal/translate/export/rdf_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,14 @@ type writing struct {
// first: they are what tells an owned Expression from an expression node, and
// what owns an element whose graph states ownership from the membership alone.
func (d *decoder) build() ([]*element, error) {
stubs, err := d.libraryStubs()
if err != nil {
return nil, err
}
for _, subject := range d.graph.Subjects() {
if stubs[subject.Value] {
continue
}
if d.isMembership(subject) {
if err := d.readMembership(subject); err != nil {
return nil, err
Expand All @@ -736,7 +743,7 @@ func (d *decoder) build() ([]*element, error) {
}
var order []*element
for _, subject := range d.graph.Subjects() {
if d.isMembership(subject) || d.isExpressionNode(subject) || d.isFilterPackageNode(subject) {
if stubs[subject.Value] || d.isMembership(subject) || d.isExpressionNode(subject) || d.isFilterPackageNode(subject) {
// A node of an expression graph belongs to the declaration that holds
// the expression, not to an element of its own; so does a filter package.
continue
Expand Down Expand Up @@ -1235,6 +1242,65 @@ func (d *decoder) referencedElement(iri string) (*element, error) {
return target, nil
}

// libraryStubs finds the subjects that name a standard library element the
// graph references (sysml:isLibraryElement, and no owner in the graph): they
// are references into the bundled library, not declarations, so they are not
// written back. Each one's qualified name must be the one the library gives its
// id, and a membership one must own such an element; a stub the library does
// not know, or names otherwise, is refused rather than trusted.
func (d *decoder) libraryStubs() (map[string]bool, error) {
stubs := map[string]bool{}
for _, subject := range d.graph.Subjects() {
if !LibraryReference(d.graph, subject) {
continue
}
id := rdf.LocalName(subject.Value)
if stated, ok := d.graph.Lexical(subject, rdf.SysML+pElementID); ok {
id = stated
}
if members := d.graph.Objects(subject, rdf.SysML+pMemberElement); len(members) > 0 {
// A library membership is the owning membership the library gives
// its id, and owns the element the library says it owns.
catalog := identity.LibraryCatalog(libs.NewModelIndex())
owned, ok := catalog.OwningMembership(id)
if !ok {
return nil, &UnsupportedError{
What: fmt.Sprintf("the library membership <%s>", subject.Value),
Note: fmt.Sprintf("it is marked sysml:isLibraryElement, but the bundled standard library has no owning membership with id %q", id),
}
}
member := members[0]
memberID := rdf.LocalName(member.Value)
if stated, ok := d.graph.Lexical(member, rdf.SysML+pElementID); ok {
memberID = stated
}
if len(members) != 1 || !member.IsIRI() || !d.graph.BoolValue(member, rdf.SysML+pIsLibraryElement) || memberID != owned.ID {
return nil, &UnsupportedError{
What: fmt.Sprintf("the library membership <%s>", subject.Value),
Note: fmt.Sprintf("the bundled standard library gives id %q to the owning membership of %s (id %q), and its memberElement is not that library element", id, owned.FQN, owned.ID),
}
}
stubs[subject.Value] = true
continue
}
lib, ok := d.libraryElement(subject.Value, id)
if !ok {
return nil, &UnsupportedError{
What: fmt.Sprintf("the library element <%s>", subject.Value),
Note: fmt.Sprintf("it is marked sysml:isLibraryElement, but the bundled standard library has no element with id %q", id),
}
}
if stated, ok := d.graph.Lexical(subject, rdf.SysML+pQualifiedName); ok && stated != lib.qname {
return nil, &UnsupportedError{
What: fmt.Sprintf("the library element <%s>", subject.Value),
Note: fmt.Sprintf("the graph names it %s, but the bundled standard library gives id %q to %s", stated, id, lib.qname),
}
}
stubs[subject.Value] = true
}
return stubs, nil
}

// libraryElement is the standard library element whose normative id a reference
// names, if the norm fixes id for one; the graph need not define it.
func (d *decoder) libraryElement(iri, id string) (*element, bool) {
Expand Down
91 changes: 91 additions & 0 deletions internal/translate/export/rdf_out.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
// Property names in the SysML vocabulary.
const (
pDeclaredName = "declaredName"
pIsLibraryElement = "isLibraryElement"
pDeclaredShortName = "declaredShortName"
pQualifiedName = "qualifiedName"
pElementID = "elementId"
Expand Down Expand Up @@ -398,6 +399,7 @@ func (e *encoder) encodeDocument(root *ast.RootNamespace) error {
if e.idErr != nil {
return e.idErr
}
e.libraryNames()
e.sourceText()
if err := rdf.AnnotateCollections(e.graph); err != nil {
return err
Expand Down Expand Up @@ -485,6 +487,9 @@ func newEncoderOver(file *source.SourceFile, root *ast.RootNamespace, form IDFor
}

type encoder struct {
// libraryRefs are the standard library elements the document links to, by
// subject IRI, so the graph can name the ones it references.
libraryRefs map[string]libraryRef
// minted lists the subjects minted for this document's declarations, which a
// model of several documents checks no other document declares too.
minted []mintedSubject
Expand Down Expand Up @@ -653,6 +658,84 @@ func (e *encoder) importedMembership(name *ast.QualifiedName) rdf.Term {
return rdf.String(qualifiedText(name))
}

// libraryRef is a standard library element the document links to: its
// declaration, qualified name and normative subject, and the subject of its
// owning membership where the norm fixes one.
type libraryRef struct {
node ast.Node
fqn string
subject, membership rdf.Term
}

// libraryNames states, for each standard library element the graph references,
// what the graph cannot otherwise say about it: its metaclass and qualified
// name, marked isLibraryElement (KerML Element::isLibraryElement), so a reader
// without the library can name what a normative id stands for. The library
// itself is not exported: the element's members, relationships and owner stay
// in the library. A library membership the graph references (an import of a
// library member) is stated as the OwningMembership of that element.
func (e *encoder) libraryNames() {
if len(e.libraryRefs) == 0 {
return
}
referenced := map[string]bool{}
for _, t := range e.graph.Triples() {
if t.Object.IsIRI() {
referenced[t.Object.Value] = true
}
}
subjects := make([]string, 0, len(e.libraryRefs))
for iri := range e.libraryRefs {
subjects = append(subjects, iri)
}
slices.Sort(subjects)
named := map[string]bool{}
// name states ref's element, reporting whether it is named in the graph.
name := func(ref libraryRef) bool {
if done, seen := named[ref.subject.Value]; seen {
return done
}
named[ref.subject.Value] = false
metaclass := declaredMetaclass(ref.node)
switch n := ref.node.(type) {
case *ast.Package:
// `standard library package` (KerML 1.0 § 8.3.4.13.3 LibraryPackage).
metaclass = mPackage
if n.IsLibrary {
metaclass = mLibraryPackage
}
case *ast.Alias:
// An alias is a Membership, which the graph names by the membership
// it is rather than as an element.
return false
}
if metaclass == "" {
return false
}
named[ref.subject.Value] = true
e.graph.Add(ref.subject, rdf.IRI(rdf.RDFType), e.sysml(metaclass))
e.graph.Add(ref.subject, e.sysml(pElementID), rdf.String(rdf.LocalName(ref.subject.Value)))
e.graph.Add(ref.subject, e.sysml(pQualifiedName), rdf.String(ref.fqn))
if declared, _ := declaredNameAndMembers(ref.node); declared != "" {
e.graph.Add(ref.subject, e.sysml(pDeclaredName), rdf.String(declared))
}
e.graph.Add(ref.subject, e.sysml(pIsLibraryElement), rdf.Bool(true))
return true
}
for _, iri := range subjects {
ref := e.libraryRefs[iri]
if referenced[iri] {
name(ref)
}
if ref.membership.Value != "" && referenced[ref.membership.Value] && name(ref) {
e.graph.Add(ref.membership, rdf.IRI(rdf.RDFType), e.sysml(mOwningMembership))
e.graph.Add(ref.membership, e.sysml(pElementID), rdf.String(rdf.LocalName(ref.membership.Value)))
e.graph.Add(ref.membership, e.sysml(pMemberElement), ref.subject)
e.graph.Add(ref.membership, e.sysml(pIsLibraryElement), rdf.Bool(true))
}
}
}

// claimLibrary reserves the IRIs of a library element the document links to,
// and of its owning membership, so no element declared here lands on them.
func (e *encoder) claimLibrary(node ast.Node, fqn string) {
Expand All @@ -663,6 +746,14 @@ func (e *encoder) claimLibrary(node ast.Node, fqn string) {
e.ids.owningMembershipOf(node, subject).Value, fqn + "'s owning membership",
})
}
if e.libraryRefs == nil {
e.libraryRefs = map[string]libraryRef{}
}
ref := libraryRef{node: node, fqn: fqn, subject: subject}
if e.ids.normativeMembership(node) {
ref.membership = e.ids.owningMembershipOf(node, subject)
}
e.libraryRefs[subject.Value] = ref
for _, c := range claims {
if prior, taken := e.claim(c.iri, c.standsFor); taken && e.idErr == nil {
e.idErr = &UnsupportedError{
Expand Down
12 changes: 11 additions & 1 deletion internal/translate/export/root_namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,24 @@ func unownedElements(graph *rdf.Graph) []rdf.Term {
if !strings.HasPrefix(subject.Value, rdf.Element) || !strings.HasPrefix(graph.Type(subject), rdf.SysML) {
continue
}
if hasOwner(graph, subject) {
// A library element the graph names is owned in the library, not
// here: the document's namespace does not take it.
if hasOwner(graph, subject) || LibraryReference(graph, subject) {
continue
}
roots = append(roots, subject)
}
return roots
}

// LibraryReference reports whether subject names a standard library element
// (or its owning membership) the graph references rather than declares: marked
// sysml:isLibraryElement, with no owner in the graph.
func LibraryReference(graph *rdf.Graph, subject rdf.Term) bool {
return graph.BoolValue(subject, rdf.SysML+pIsLibraryElement) && !hasOwner(graph, subject) &&
!graph.HasProperty(subject, rdf.SysML+pMembershipOwningNamespace)
}

func hasOwner(graph *rdf.Graph, subject rdf.Term) bool {
return graph.HasProperty(subject, rdf.SysML+pOwner) ||
graph.HasProperty(subject, rdf.SysML+pOwningRelationship) ||
Expand Down
11 changes: 10 additions & 1 deletion internal/translate/interop/reposync/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,17 @@ func viewOf(g *rdf.Graph, rep Carrier) (map[string]*subjectView, []UncarriedProp
}
views := map[string]*subjectView{}
uncarried := map[string]int{}
// A library element the graph names (isLibraryElement) is a reference into
// the standard library, not an element of the model: it is never created,
// changed or deleted in a repository.
library := map[string]bool{}
for _, subject := range g.Subjects() {
if export.LibraryReference(g, subject) {
library[subject.Value] = true
}
}
for _, triple := range g.Triples() {
if !triple.Subject.IsIRI() {
if !triple.Subject.IsIRI() || library[triple.Subject.Value] {
continue
}
id := rdf.LocalName(triple.Subject.Value)
Expand Down
5 changes: 5 additions & 0 deletions tests/export/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3291,6 +3291,11 @@ func TestFixtureElementIDsRoundTrip(t *testing.T) {
t.Fatalf("%s: %v", path, err)
}
for _, subject := range graph.Subjects() {
// A library element the graph names keeps the normative id the
// references to it carry; its name is checked against the library.
if export.LibraryReference(graph, subject) {
continue
}
if strings.HasPrefix(subject.Value, rdf.Expression) && strings.HasSuffix(subject.Value, "_om") {
if _, ok := graph.Object(subject, rdf.SysML+"memberElement"); !ok {
t.Errorf("%s: expression membership %s has no member", path, subject.Value)
Expand Down
Loading
Loading