feature ::= featureAnnotation* ( "trait" | "final" "class" ) ID_feature "extends" ( "Feature" | featureType ) "{" ( attribute [ "=" initialization ] )* "}" [ invariantObject ] featureAnnotation ::= featureLevel [ "(" <scalaExp : java.lang.String(S)> ")" ] | "@Data" | "@Settable" featureLevel ::= "@Schema" | "@Class" | "@Product"| "@Device" | "@Instance" featureType ::= ID_feature ( "with" ID_feature )* attribute ::= attributeAnnotation* [ "final" ] "val" ID_attribute ":" type attributeAnnotation ::= "override" | "@Data" | "@Settable" | "@Const" [ "(" constLevel | "value" "=" constLevel "," "qualifier" "=" <scalaExp : java.lang.String(S)> ")" ] | "@Multiplicity" "(" "lo" "=" <scalaExp : scala.Int(N)> [ "," "hi" "=" ( <scalaExp : scala.Int(N)> | "*" ) ] [ "," "clas" "=" "classOf" "[" <typeName> "]" ] ")" constLevel ::= "SCHEMA" | "CLASS" | "PRODUCT" | "INSTANCE" | "UNSPECIFIED" invariantObject ::= "object" ID_feature "{" invariant* "}" invariant ::= "@Inv" "val" ID_invariant ":" "Predicate" "[" predicateType "]" "=" "pred" "{" ID ":" predicateType "=>" <scalaExp : dms.Boolean> "}" predicateType ::= featureType | "(" featureType ( "," featureType )+ ")" requirement ::= [ "@Req" ] "trait" ID_requirement "{" attribute* "}" "object" ID_requirement "{" invariant* "}"
Feature declarations are used to introduce compound types consisting of a set of named attributes for modeling parts or complete medical device features. Attributes are used to model information that may or must present in devices. Moreover, features can declare invariants over attribute values that express information consistency constraints. For example, the dms.example.schema.Range feature is used to model a number range information between min and max (inclusive), and an invariant that states that min is less than or equal to max. (Note that in Scala, backticks can delimit unconventional identifiers).
Requirement declarations are used to introduce constraints for features that depend on other features, which are useful for medical device coordination. For example, the dms.example.requirement.MyReqPulseOx states a requirement for a pulse oximeter that has the specific SpO2 and pulse rate ranges.
Similar to basic types, feature sub-type hierarchy forms a lattice with dms.Feature at the top. By allowing multiple feature inheritance (featureType), one can mix-in different features to put together larger device parts or complete device features. As stated in the Well-Formedness Section below, we avoid the dreaded diamond problem associated with multiple inheritance over feature attributes by disallowing it completely instead of allowing mitigations or workarounds.
DML (DMS) recognize four main levels of features for staging device type refinement and when constant attribute values should be provided:
At each level, one can add a String qualifier ([ "(" <scalaExp : java.lang.String(S)> ")" ]) that indicates custom sub-levels within the provided four levels; by default, the qualifier is an empty string ("").
In addition to specifying feature level at each feature declaration, one can dedicate a Scala/Java package for a particular level and annotate the package with @Schema, @Class, @Product, or @Device instead.
Unfortunately, Scala does not support annotation on packages, thus, this should be done using Java’s package-info.java facility. For example, see the following package infos:
The package annotation scheme sets the default level for any feature declared in the package; this default can still be overriden by specifying the level in a feature declaration.
If the feature or package level are not provided, the level is considered as Unspecified.
The @Data annotation indicates that the declared feature’s attributes form inter-device communication structures. Data features should not contain @Settable attributes or attributes whose type contains a @Settable feature. For example, see the various features in dms.example.schema.Schema.scala starting with the MetricAttribute feature.
The @Settable annotation indicates that the feature attribute values can be assigned; by default, attribute values are read-only. Settable features should not contain @Data or @Const attributes or attributes whose type contains @Data or @Const. For example, see the various features in dms.example.schema.Schema.scala starting with the RangeSetting feature.
As mentioned previously, attributes are used to model information stored in devices. Attribute types can be either basic type, feature, or other compound types such as a sequence or a set as described in the Type and Initialization Section. Moreover, an attribute declaration can be accompanied with an initial attribute value.
Attributes are inherited from super-types to their sub-types. A sub-type can refine the attribute type inherited from its super-type. However, no two or more attributes with the same name whose declaring features are different can be inherited (attribute refinements are considered as declarations).
In addition to @Data and @Settable similar to feature, attributes can be annotated as overriding previously declared attribute (override), constants (@Const), or multiplity constraints (@Multiplicity) for attributes whose type is a set and or a sequence. Note that override should be listed last to adhere to Scala’s grammar.
The override annotation expressed as a Scala modifier is used to indicate that the declared attribute is a refinement of attributes that have been declared by one of the feature’s super-types. The dms.ModelExtractor auto-detects overriding attributes, however, it is strongly recommended that override is explicitly specified for documentation purpose and to prevent specification mistakes (e.g., typos) that inadvertently lead to introductions of new attributes instead of refining existing ones.
There are three reasons why one wants to override an attribute:
The constant annotation indicates that the attributes hold non-changing information after they are initialized. The initialization level (and sub-level qualifier) can be specified to enforce that the constant value is assigned at a specific level (and sub-level, if provided). For example, see the type attribute of dms.example.schema.ICEDevice that specifies its constant value has to be provided at the Class level (without qualifier, which means at any sub-level before it is used in the Product/Device level). This is satisfied, for example, by the dms.example.clas.ICEPulseOx feature. A declaration of a constant attribute with initialization should be declared as final.
For attribute whose type is a sequence or a set (collection), it may contain zero or more elements. DML (DMS) provide a specialized, lightweight construct useful for constraining the size of sequence/set. For example, see dms.example.clas.ICEPulseOx.physioParams. Each multiplicity constraint can specify the low and high bounds (inclusive) for the number of elements in the sequence/set (as usual, the low bound should be equal or less than the high bound); by default, the low bound is 0, and the high bound is unbounded (*). In addition, one can specify the element type that is applicable for the multiplicity constraint. If specified, it means that the low and high bounds are on the number of elements whose type is a sub-type of the specified type. For example, the multiplicity constraint dms.example.clas.ICEPulseOx.physioParams specifies that dms.example.clas.ICEPulseOx.physioParams should at least have one dms.example.clas.ICEPulseOx. If the type is unspecified, then by default, it is Object, which match any element type.
The multiplicity constraint can be expressed using a general invariant construct (described below) such as the general invariants for dms.example.clas.ICEPulseOx. However, using the annotation eases the development of some tool support, for example, a tool that generates UML class diagrams. Thus, multiplicity constraints should be used first whenever possible instead of expressing them as general constraints.
Invariant declarations are used to state feature consistency constraints. Instead of declaring them in the feature trait or class, invariants are declared in the feature companion object (invariantObject).
Invariants are inherited from super types to their sub-types. However, they are not allowed to be overriden. That is, a sub-type cannot declare an invariant with the same name as with any of its super types’ invariants. The effective invariant of a feature is a conjunction of all the invariants declared by the feature and all invariants inherited from its super types.
In DMS, each invariant declaration is represented using a val that is annotated with @Inv and whose type (predicateType) is a dms.Predicate over the type of the feature (dms.Predicate returns dms.Boolean, i.e., not to be confused with scala.Boolean). For example, dms.example.schema.Range’s invariant should have the type of a dms.Predicate over dms.example.schema.Range, i.e., dms.Predicate[dms.example.schema.Range].
The invariant val should be initialized by calling the dms.pred macro that accepts a scala.Function1 (that in turn, accepts a feature type object and returns a dms.Boolean object). While any Scala expression returning a function from the feature type to dms.Boolean would be accepted, one should use the Scala anonymous function syntax variant as described in the invariant grammar.
The body of the function (<scalaExp : dms.Boolean>) cannot depend on variables declared outside the function (i.e., no free variables are allowed). The exact expression language subset is not specified at this point in time. Currently, we are considering the following subset:
As DML (DMS) are evolved, we will settle on the invariant expression language.
Note
The dms.pred macro is used to retrieve the fully-resolved Scala AST of the invariant function expression that works similarly to scala.reflect.api.Universe‘s reify; the difference is that dms.pred enforces the expression’s type to be a function type returning dms.Boolean. The fully-resolved Scala AST is then retrieved by the dms.ModelExtractor during model extraction process.
Below is a table that maps grammar productions related to feature and its DML AST representation classes:
Grammar Non-Terminals | DML AST Classes |
---|---|
feature | dml.ast.Feature with name = fully-qualified name of the feature |
featureAnnotation | |
featureLevel | dml.ast.FeatureLevel |
featureType | dml.ast.NamedType (list of) |
attribute | dml.ast.Attribute |
attributeAnnotation | |
constLevel | dml.ast.FeatureLevel |
invariantObject | not represented; invariants stored directly in dml.ast.Feature members |
invariant | dml.ast.Invariant |
Currently, dml.ast.Invariant predicates are not represented using custom DML AST classes (the predicate type is java.lang.Object); the predicate is currently represented using Scala scala.reflect.api.Trees types. As DML (DMS) expression language is evolved, we will introduce custom AST classes for representing predicates.
There are several methods provided by the symbol table API related to features. Below is the list of relevant methods (please see the documentation in the dml.symbol.SymbolTable that describes the methods):
Requirements are used to express dependency constraints for medical device coordinations. That is, if a device requires some features from other devices with some specific properties, the device can advertise the properties as requirements.
A requirement consists of attributes and invariants that state the required feature properties. The form of a requirement invariant is similar to feature`s invariant, except that a requirement invariant predicate function is allowed to work over a tuple of feature types (predicateType: "(" `featureType` ( "," `featureType` )+ ")").
Requirements are represented using the dml.ast.Requirement AST class (with name = fully-qualified name of the requirement) and requirement invariant is represented using dml.ast.Invariant similar to a feature invariant.
There are several methods provided by the symbol table API related to requirements. Below is the list of relevant methods (please see the documentation in the dml.symbol.SymbolTable that describes the methods):