model ::= package imports declaration* package ::= "package" ( ID "." )* ID imports ::= importdms import* importdms ::= "import" "edu" "." "ksu" "." "cis" "." "santos" "." "mdcf" "." "dms" "." "_" import ::= "import" ( ID "." )* ID [ "." "_" ] declaration ::= basicType | feature | requirement
A model may be specified in multiple Scala source files. For each file, a model source starts with a package declaration, followed by import declarations and declarations of basic types, features, or requirements. That is, it is assumed that models are specified in a package other than the Scala or Java “default” package. The grammar recommends importing all elements (specified using _ instead of * like in Java) defined in the dms Scala package and dms.package package object, which defines DMS primordial types, implicit conversions, and a macro, which will be described in the appropriate subsequent sections below.
In addition, it recommends importing basic types or features defined in different packages; note that Scala allows import declarations to appear in many places, including inside class declarations and expression blocks among others.
One can alternatively choose to not import any package elements and always use the fully qualified name of package elements.
Some examples of Scala model files can be found at:
We will use portions of the above examples to illustrate various DMS elements in the subsequent sections.
A model is represented using the dml.ast.Model AST class which has an Iterable of dml.ast.Declarations, which can be either dml.ast.BasicType, dml.ast.Feature, or dml.ast.Requirement.
One can construct a model by calling the dml.ast.Ast model static method as illustrated in the following example:
1 2 3 4 5 6 7 8 9 10 11 12 | import org.junit.Test;
import edu.ksu.cis.santos.mdcf.dml.ast.*;
import static edu.ksu.cis.santos.mdcf.dml.ast.Ast.*;
public class ExModel {
@Test
public void construct() {
Model m = model(Ast.<Declaration>list()); // create a model with empty declarations
System.out.println(m); // output: model(list())
System.out.println(m.declarations); // output: []
}
}
|
The above example creates a model with an empty list of declarations, and then prints out the model and the model’s declarations. The dml.ast.Ast list methods are helper methods that when given either a variable or an Iterable number of objects, they create an (immutable) list containing the provided objects.
Notice that at line 8, we need to use Ast.<Declaration> list() instead of just list(). That is because list() without a parameter type supplied for its element type returns List<Object>, which is incompatible with model‘s parameter that expects a list of dml.ast.Declaration. To address this issue, one can use dml.ast.Ast.Weak API that have the same set of AST construction methods, but with weaker compile-time parameter types that are checked at runtime; IllegalArgumentException will be thrown if the runtime types are not what expected. Below is an example that illustrates the use of dml.ast.Ast.Weak API:
1 2 3 4 5 6 7 8 9 10 11 12 | import org.junit.Test;
import edu.ksu.cis.santos.mdcf.dml.ast.*;
import static edu.ksu.cis.santos.mdcf.dml.ast.Ast.Weak.*;
public class ExModelWeak {
@Test
public void construct() {
Model m = model(list()); // create a model with empty declarations
System.out.println(m); // output: model(list())
System.out.println(m.declarations); // output: []
}
}
|
Below is a similar example written in Scala; thanks to Scala’s type inference, dml.ast.Ast API work without a problem as can be seen at line 13 (of course dml.ast.Ast.Weak would work as well):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.FunSuite
import edu.ksu.cis.santos.mdcf.dml.ast._
import Ast._
@RunWith(classOf[JUnitRunner])
class ExsModel extends FunSuite {
test("Empty model construction") {
val m = model(list()) // create a model with empty declarations
println(m) // output: model(list())
println(m.declarations) // output: []
val m2 = org.sireum.util.Reflection.eval[Model](s"""
import edu.ksu.cis.santos.mdcf.dml.ast.Ast._
${m.toString}
""")
println(m2) // output: model(list())
}
}
|
We mentioned previously in the String Representation Section that the AST toString method produces a code that construct structurally equivalent AST. This is illustrated at lines 17-21 where m‘s toString is used to construct a Scala code as String at line 19 (s" ... " is a Scala string interpolator) along with the import declaration at line 18; Reflection‘s eval method takes the String code, compiles it (at runtime), and then invokes the compiled code to produce a structurally equivalent model m2 with respect to m as shown in at line 21.
Note
AST construction methods are named the same as the AST class names that they build but with the first letter lowercased.
Instead of constructing AST manually by hand, dms.ModelExtractor provides extractModel “static” methods that extract DML AST from compiled DMS model Java class files (see the note below). Given an array of package String names, they return the DML AST model that is represented in the packages and their sub-packages.
Below is an example illustrating the use of dms.ModelExtractor API to extract DMS models from the dms.example package and its sub-packages:
1 2 3 4 5 6 7 8 9 10 11 | import org.junit.Test;
import edu.ksu.cis.santos.mdcf.dml.ast.*;
import static edu.ksu.cis.santos.mdcf.dms.ModelExtractor.*;
public class ExModelExtractor {
@Test
public void construct() {
Model m = extractModel(new String[] { "edu.ksu.cis.santos.mdcf.dms.example" } );
System.out.println(m); // output: model(list(basicType(...)))
}
}
|
Variants of the dms.ModelExtractor extractModel methods are provided to extract a model using a specific ClassLoader and dms.ModelExtractor Reporter for custimizing warning and error notifications during the extraction process. By default, the ClassLoader that loads the dms.ModelExtractor is used, and warning and errors are printed to the console (output and error streams, respectively).
Note
The dms.ModelExtractor extracts models from Java bytecode compiled from DMS model source files. Thus, one need to compile the Scala source files first by using the Scala compiler. After compilation, the dms.ModelExtractor does not require access to the source code.
When extracting a model, make sure that the Java classpath is setup properly so the dms.ModelExtractor can find the package elements. Otherwise, it would return a model with empty declarations. The dms.ModelExtractor will give a warning when such situation occurs.
The dms.ModelExtractor is implemented using a combination of Scala Runtime Reflection, Guava ClassPath, and Java Reflection, including Java dynamic Proxy. Interested reader is referred to the dms.ModelExtractor code and Sireum’s Reflection code.
Given a set of models, the dml.symbol.SymbolTable of method creates an instance of dml.symbol.SymbolTable that provides various methods to retrieve DML entities such as basic types, features, attributes, and invariants in the models or by their fully-qualified name.
Below is an example to illustate the construction of dml.symbol.SymbolTable and its String representation:
1 2 3 4 5 6 7 8 9 10 11 12 | import org.junit.Test;
import edu.ksu.cis.santos.mdcf.dml.symbol.SymbolTable;
import static edu.ksu.cis.santos.mdcf.dms.ModelExtractor.*;
public class ExModelSymbolTable {
@Test
public void construct() {
SymbolTable st = SymbolTable
.of(extractModel(new String[] { "edu.ksu.cis.santos.mdcf.dms.example" }));
System.out.println(st); // output: SymbolTable.of(model(list(basicType(...))))
}
}
|
The reader is referred to the dml.symbol.SymbolTable Javadoc to see what queries are available through the API; we will mention some of the API in the relevant subsequent sections.
The dml.serialization.XStreamer API provides AST and symbol table serialization to XML and deserialization back from XML using XStream with custom converters that makes the produced XML representation easier to read as well as its size. More specifically, it provides toXml and fromXml static methods for converting to/from String, Writer/Reader, or OutputStream/InputStream.
Below is an example that uses the dml.serialization.XStreamer API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import org.junit.Test;
import edu.ksu.cis.santos.mdcf.dml.ast.*;
import edu.ksu.cis.santos.mdcf.dml.symbol.SymbolTable;
import static edu.ksu.cis.santos.mdcf.dml.ast.Ast.Weak.*;
import static edu.ksu.cis.santos.mdcf.dml.serialization.XStreamer.*;
import static edu.ksu.cis.santos.mdcf.dms.ModelExtractor.extractModel;
public class ExModelXml {
@Test
public void de_serialization() {
Model m = model(list()); // create a model with empty declarations
String mXml = toXml(m);
System.out.println(mXml); // output: <model>
// <declarations class="ilist"></declarations>
// </model>
System.out.println(fromXml(mXml)); // output: model(list())
SymbolTable st = SymbolTable
.of(extractModel(new String[] { "edu.ksu.cis.santos.mdcf.dms.example" }));
String stXml = toXml(st);
System.out.println(stXml); // output: <symbolTable>
// <models class="ilist">
// <model>
// <declarations class="ilist">
// <basicType ...
System.out.println(fromXml(stXml)); // output: SymbolTable.of(model(list(basicType(...))))
}
}
|
The dml.serialization.XStreamer API also provides serialization to JSON provided by its toJson methods that use XStream along with Jettison. Deserialization from Json using dml.serialization.XStreamer fromJson static methods is, however, currently not supported (there seems to be a deserialization bug in XStream with Jettison that warrants further investigations).
In addition, a custom XStream instance can be retrieved by calling the dml.serialization.XStreamer xstream static method.
The dml.ast.IVisitor interface and dml.ast.AbstractVisitor class realize the visitor pattern for (top-down, left-to-right) traversal of DML AST. Each visit method for different AST node type in dml.ast.IVisitor returns a boolean value that, if true, indicates the visitor should continue visiting the node’s children; otherwise, the traversal is short-circuited by skipping the node’s children (and hence, the node’s decendants).
The visit methods for abstract AST node types (i.e., dml.ast.Declaration, dml.ast.Initialization, dml.ast.Member, and dml.ast.Type) will be visited first before any of its subtypes visit methods are called. For example, dml.ast.IVisitor visitDeclaration will be called before visitBasicType. In such case, the AST traversal continues visiting the node’s children when both visitDeclaration and visitBasicType return true.
The dml.ast.AbstractVisitor class provides a basic implementation of dml.ast.IVisitor where calls to all visit methods of non-abstract AST node types are routed to call (and return the return value of) dml.ast.AbstractVisitor defaultCase, which can be conveniently overriden in a sub-class.
Below is an example to illustrate how one can traverse DML AST using the provided visitor API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import org.junit.Test;
import edu.ksu.cis.santos.mdcf.dml.ast.*;
import static edu.ksu.cis.santos.mdcf.dml.ast.Ast.Weak.*;
public class ExModelVisitor {
@Test
public void visit() {
Model m = model(list(basicType("foo", list())));
System.out.println(m); // output: model(list(basicType("foo", list())))
new AbstractVisitor() {
@Override
public boolean visitBasicType(BasicType node) {
System.out.println("Reached " + node.name); // output: Reached foo
return true;
}
}.visit(m);
new AbstractVisitor() {
@Override
public boolean visitModel(Model node) {
return false;
}
@Override
public boolean visitBasicType(BasicType node) {
throw new RuntimeException("Reached " + node.name); // unreachable
}
}.visit(m);
new AbstractVisitor() {
@Override
public boolean defaultCase(AstNode node) {
System.out.println(node); // output: model(list(basicType("foo", list())))
// basicType("foo", list())
return true;
}
}.visit(m);
}
}
|
The example creates a model with one basic type. The first visitor at lines 12-18 prints out the name of any basic type it encounters at line 15, which in this case prints out Reached foo. The second visitor at lines 20-30 throws an exception at line 28 whenever it encounters a basic type; however, because its visitModel method returns false, the visitor skips all model’s children, hence skipping visiting the basic type declaration (more precisely, all declarations). The third visitor at lines 32-39 prints out AST node in its defaultCase method; hence, it prints out the model and the basic type.
A set of models is well-formed if all user-defined basic types, features, and requirements are well-formed.