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:

  • cast and try_cast methods for casting the node to a specific type.

  • document attribute for accessing the document the node belongs to.

  • parent attribute for accessing the parent node.

  • isinstance method for checking if the node is an instance of a specific type.

  • owned_elements attribute for accessing the owned elements of the node.

  • cst_node attribute 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.children and Dependency.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.prefixes and Dependency.prefixes represent a group for metadata prefixes, prefixed with # in textual notation.

  • Type.type_relationships represents non-specialization type relationships appearing after specialization part, including feature chaining.

  • Type.heritage represents specialization and conjugation type relationships.

  • Connector.declared_ends and FlowUsage.declared_messages are end features and messages before the children group. In the textual syntax they appear in the same position, hence in contrast to similar groups there are additional try_append and try_insert methods that return None without 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 _member properties, e.g. feature_value_member. Commons accessors are MemberAccessor for working with non-owned member elements, OwnedMemberAccessor – for working with owned member elements, and ChainedMemberAccessor – 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 _target properties on select relationships, e.g. Subsetting. The common base class is ReferenceAccessor.

Constraints

When modifying the model, the following constraints must be satisfied: