Model Structure
A Document is the smallest atomic model piece in Syside
which corresponds to a single source file. For performance reasons, any
Elements are allocated and destroyed by the owning
Document. See Low-Level API for more details.
Because Syside is developed with multithreading in mind, each Document is protected by a mutex even if it is a noop in single-threaded
environments. This is carried over into Python to provide identical API access for
potential free-threaded Python builds in the future.
Note
For ease-of-use, accessing all referenced elements and their documents does not require locking. Instead, any editor-like applications need to lock all related documents explicitly together to prevent races. This is done in Syside LSP implementation.
Locking happens through context manager interface on
SharedMutex:
with mutex.lock() as doc:
pass
Locking multiple mutexes needs ExitStack:
from contextlib import ExitStack
with ExitStack() as stack:
documents = [stack.enter_context(mutex.lock()) for mutex in mutexes]
Note
preview.open_model takes care of locking
mutexes but its interface may be incomplete and change more frequently.
Note
It is recommended to write your analysis code methods as taking Documents rather than SharedMutex[syside.Document] – locking is only needed at the script or thread entry
points. This is how Syside does it internally.
Types
The model types follows KerML and SysML specifications, with types mapping one-to-one to
Syside types, see Element and its children.
Note
For performance reasons, the model does not use interface base classes and multiple
inheritance. While all missing specification attributes are implemented even if a
corresponding class is not a base, isinstance checks may work differently than
expected from the specification types. Instead, use STD class variables, e.g.
ActionUsage.STD, to match specification
behaviour:
if isinstance(element, syside.ActionUsage.STD):
...
For type-checking, there is a corresponding Std class type alias that is only
defined during TYPE_CHECKING:
def example(element: syside.Element) -> syside.Connector.Std:
return element.cast(syside.Connector.STD)
This distinction is required due to limitations of the Python type system which does not allow type aliases and variables to be bound to the same name.
Attributes
Throughout the API, all attributes and methods use snake_casing to match Python
naming convention. This is in contrast to camelCasing used by the specification
coming from Java naming conventions. For example, attribute
AssertConstraintUsage::assertedConstraint is mapped to
AssertConstraintUsage.asserted_constraint.
Additionally, even if an attribute is defined as returning something in the
specification, i.e. with a multiplicity lower bound of 1, Syside usually returns an
optional value. This is because for IDE analysis, even partial models are useful, and
they often have syntax errors which result in required members not being present.
Additionally, attributes that can return multiple values often return
LazyIterator instead which traverses and collects
elements lazily – elements instead need to be collected by calling .collect(), e.g. on Namespace.owned_members.
Lastly, AstNode provides some convenience attributes:
castandtry_castmethods for casting the node to a specific type.documentattribute for accessing the document the node belongs to.parentattribute for accessing the parent node.isinstancemethod for checking if the node is an instance of a specific type.owned_elementsattribute for accessing the owned elements of the node.cst_nodeattribute for accessing the concrete syntax node corresponding to the node.Warning
This should not be stored for long as it may go out of scope and be deleted after a document is reparsed.
Modifications
Syside tries to enforce the invariant that all owned relationships have at least two related elements. This is achieved by requiring the relationship type when adding any child elements or references which also allows checking that the related element type is valid for that relationship.
For convenient modification of the model, Syside provides a set of additional properties and methods on the abstract syntax classes. Most commonly used ones are:
Namespace.childrenandDependency.children(including other specific relationships) represent and allow modifying elements in the body of the element – in the textual notation, between brackets{and}and expression arguments, e.g.:mem, element = namespace.children.append( syside.OwningMembership, syside.Package ) assert namespace.children.remove_element(element) assert not mem.parent
Namespace.prefixesandDependency.prefixesrepresent a group for metadata prefixes, prefixed with#in textual notation.Type.type_relationshipsrepresents non-specialization type relationships appearing after specialization part, including feature chaining.Type.heritagerepresents specialization and conjugation type relationships.Connector.declared_endsandFlowUsage.declared_messagesare end features and messages before thechildrengroup. In the textual syntax they appear in the same position, hence in contrast to similar groups there are additionaltry_appendandtry_insertmethods that returnNonewithout throwing if modification failed because the slot is already occupied by another group.Members with specific positions in the textual syntax are often modifiable through
_memberproperties, e.g.feature_value_member. Commons accessors areMemberAccessorfor working with non-owned member elements,OwnedMemberAccessor– for working with owned member elements, andChainedMemberAccessor– for working with members that accept feature chains. Note that their subtypes are only used for improved IDE experience as Python does not yet support dependent generic type constraints.Similarly, references are modifiable through
_targetproperties on select relationships, e.g.Subsetting. The common base class isReferenceAccessor.
Constraints
When modifying the model, the following constraints must be satisfied:
An element can have only a single owner. Violating this constraint raises
ValueError. However, the same element can be referenced by multiple elements, e.g.:_, element = namespace.children.append( syside.OwningMembership, syside.Package ) _, _ = namespace.children.append(syside.OwningMembership, element) # error _, _ = namespace.children.append(syside.Membership, element) # OK _, _ = namespace.children.append(syside.Membership, element) # OK
Moving an element from one document to another is not supported and will raise
ValueError, e.g.:_, element = namespace.children.append( syside.OwningMembership, syside.Package ) namespace.children.pop(0) _, _ = other.children.append(syside.OwningMembership, element) # error _, _ = namespace.children.append(syside.OwningMembership, element) # OK
Adding a new owned or referenced element must satisfy the typing constraints and will raise
TypeErrorexception if violated:_, element = namespace.children.append( syside.OwningMembership, syside.PartDefinition ) # OK _, _ = element.children.append( syside.FeatureMembership, syside.Package ) # error
An element can be added only to an element that is not removed from the model. If this constraint is violated, a
RuntimeErroris raised. The problem can be fixed by adding the parent element back to the document as an owned element:_, element = namespace.children.append( syside.OwningMembership, syside.Package ) namespace.children.pop(0) _, _ = element.children.append( syside.OwningMembership, syside.Package ) # error _, _ = namespace.children.append(syside.OwningMembership, element) # OK _, _ = element.children.append( syside.OwningMembership, syside.Package ) # OK
Adding an owned element to a relationship that can only reference elements will raise
TypeError:_, element = namespace.children.append( syside.Membership, syside.Package ) # error _, element = namespace.children.append( syside.OwningMembership, syside.Package ) # OK