1. Introduction
1.1. The ABS Language
The ABS language is an actor-based, object-oriented, executable modeling language. Its prime features are:
- Algebraic user-defined data types and side effect-free functions
-
All data (except the state of objects and future variables) is immutable, and functions are free of side effects. This makes understanding and reasoning about models easier.
User-defined data types are used for data modeling instead of objects, so ABS models are typically smaller than their Java counterparts.
- A syntax that is close to Java
-
Programmers that are used to Java can easily learn the ABS language.
- Distributed, actor-based semantics
-
Method calls are asynchronous and create a new process in the target. Processes are scheduled cooperatively and run within the scope of one object. Futures are used to synchronize with and get the result of another process.
- Interfaces for specifying object behavior
-
Similar to Java, the behavior of a class is defined by implementing zero or more interfaces with their corresponding methods.
- Safe concurrency
-
Processes run cooperatively within one object and do not have access to other objects' state, and data structures are immutable. The most common error causes of concurrent systems (aliasing, insufficient locking) are avoided by the language semantics.
- Distributed computing
-
The combination of asynchronous method calls, immutability and strong encapsulation makes it very easy to model distributed systems.
- A formal semantics and compositional proof theory
-
ABS is designed to be amenable to program analysis and verification. A variety of tools (deadlock checker, resource analysis, formal verification) have been developed.
1.2. The ABS Actor and Concurrency Model
As mentioned, ABS method calls are asynchronous and create a new process in the target, while the caller process continues to run in parallel, as shown in Figure Process call semantics. At point ①, P1 issues an asynchronous call to some object residing on another cog. In response, a new process P2 is created; P1 and P2 can run in parallel. At point ②, P1 needs the result of the method call and suspends itself. At point ③, P2 finishes and returns a value. P1’s cog then reactivates P1 to continue execution.
The paragraph above elides some details. An asynchronous method call (see Asynchronous Method Calls) produces a future variable, which is used both to synchronize with the callee process (see Await (Statement)) and to get the result (see Get). Future variables are first-class objects that can be passed along, so multiple processes can synchronize on the same future.
The processes created by method calls are scheduled cooperatively and run within the scope of the target object. Objects are grouped into COGs (Concurrent Object Groups). Each cog runs one process at a time, while processes on different cogs run in parallel, as shown in Figure Processes running inside their cogs. This means that each cog is a unit of concurrency and is in charge of scheduling the processes running on its objects. Each process runs until it suspends itself (see Await (Statement) and Unconditional Release: Suspend) or terminates, at which point the cog chooses the next process to run.
A new cog is created by creating an object with a new
expression (see Figure
Creating an object in a new cog).
An object in an existing cog is created via the new local
expression (see Figure
Creating an object in the same cog).
1.3. Error Propagation and Recovery in ABS
ABS models exceptional (unforeseen and erroneous) situations using exceptions. This section gives an overview of the language constructs that deal with exception propagation and recovery.
Exceptions occur when a process cannot continue normal execution, e.g., when
trying to divide by zero or when no pattern in a case
expression matches the
given value. Exceptions can also be thrown by the modeler via the throw
statement: Throw. Exceptions thrown implicitly or explicitly
are propagated and handled in the same way.
The modeler can define new exceptions; see Exceptions.
Exceptions can be caught and handled locally, i.e., in a lexically enclosing
try
-catch
-finally
block in the same method (see Handling Exceptions with Try-Catch-Finally).
In that case, the process continues execution and will eventually produce a
return value to its future.
In case of an unhandled exception, the future of the process does not
receive a return value; instead, it will propagate the unhandled exception to
the caller (or any process that tries to get its value). When evaluating
f.get
on a future that carries an exception instead of a normal return
value, the exception will be re-thrown; it can be handled as usual via
try
-catch
or left to propagate up the call chain of futures.
Additionally, terminating a process in the middle of execution might leave its object in an inconsistent state. To recover from this, ABS uses recovery blocks (see Classes). Unhandled exceptions are handed to the recovery block, which can take appropriate action to re-establish the class invariant and/or send asynchronous messages to other objects.
2. Lexical Structure
This section describes the lexical structure of the ABS language. We use the following EBNF conventions for specifying the syntax of ABS elements.
|
Terminal symbols (occurring in program source code) |
CursiveText |
Non-terminals (production rule names) |
::= |
Separator between left hand side and right hand side of a rule |
| |
Variant; either of the element(s) separated by | |
[ ] |
Optionals; the enclosed elements can be omitted |
{ } |
Repetition; zero or more occurrences of the enclosed elements |
? ? |
Special group (elements in the group are specified informally) |
[: :] |
A character class, as in extended regular expression syntax |
( ) |
Grouping |
2.1. Line Terminators and White Spaces
Line terminators and white spaces are defined as in Java.
LineTerminator ::= |
|
WhiteSpace ::= |
LineTerminator | |
2.2. Comments
ABS supports the two common Java-style styles of comments: end-of-line comments and block comments.
An end-of-line comment starts with two slashes, e.g., // text
. All text that follows //
until the end of the line is treated as a comment.
Comment ::= |
LineComment | BlockComment |
LineComment ::= |
|
BlockComment ::= |
|
// this is a comment
module A; // this is also a comment
A block comment is enclosed in /* */
,
e.g., /* this is a comment */
. Block comments can span
multiple lines and do not nest.
/* this
is a multiline
comment */
2.3. Identifiers
Identifiers consist of a letter followed by a sequence of letters,
numbers and underscores (_
).
ABS distinguishes identifiers and type identifiers. Identifiers start with a lower-case character, type identifiers start with an upper-case character. Identifiers name local variables, fields, methods and functions. Type identifiers name interfaces, classes, types, type constructors, deltas and products.
Identifiers can be qualified with a module name (see Modules) or simple (without module name prefix).
SimpleIdentifier ::= |
[: lower :] { [: alpha :] | [: digit :] | |
SimpleTypeIdentifier ::= |
[: upper :] { [: alpha :] | [: digit :] | |
QualifiedIdentifier ::= |
{ SimpleTypeIdentifier |
QualifiedTypeIdentifier ::= |
{ SimpleTypeIdentifier |
Identifier ::= |
SimpleIdentifier | QualifiedIdentifier |
TypeIdentifier ::= |
SimpleTypeIdentifier | QualifiedTypeIdentifier |
2.4. Keywords
The following words are keywords in the ABS language and are invalid as identifiers.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.5. Literals
A literal is a textual representation of a value. ABS supports integer
literals, floating-point literals, string literals, and the null literal.
Rational numbers are written using the division operator /
, e.g., 1/4
for
one quarter.
Strings are enclosed in double quotes ("
). Line feed in a string is written
as \n
, carriage return as \r
.
The null literal is written as null
.
Literal ::= |
IntLiteral | FloatLiteral | StringLiteral | |
IntLiteral ::= |
|
FloatLiteral ::= |
[ IntLiteral ] |
StringLiteral ::= |
|
Valid string characters are defined as in the Java language.
2.6. Annotations
Annotations consist of a syntactically valid pure expression, optionally
preceded by a type identifier (the “tag”) and a colon (:
). They can be put
in front of statements and definitions. Annotations are enclosed in square
brackets ([]
).
There can be more than one annotation in one place. When annotating a place with more than one annotation, writing the annotations in separate pairs of brackets or separated by commas in one pair of brackets produces the same effect.
AnnotationFragment ::= |
[ TypeIdentifier |
Annotation ::= |
{ |
Annotations are used to write auxiliary information that can be used by various tools. Unknown (user-defined) annotations are ignored by the tool chain. Pre-defined annotations are usually type-checked.
[Near] class Example { ... }
This is an example of annotations with a tag:
[Cost: 15, Deadline: Duration(20)] o!m();
The same annotations, written in separate brackets:
[Cost: 15] [Deadline: Duration(20)] o!m();
Annotations are associated with the following language construct. In the
examples above, the first annotation pertains to the class definition of
Example
, the second annotation pertains to the asynchronous method call
o!m()
.
In general, it is not an error to have more than one annotation with the same tag in the same place. However, some pre-defined annotations might forbid this.
3. Types
ABS has a static, nominal type system. Local variables, object fields, function parameters and method parameters are statically typed. A type name can refer to a algebraic data type, an interface, a type synonym. There are a number of pre-defined data types which are documented in Chapter The Standard Library.
A type name is a sequence of letters, digits and underscores (_
) starting
with an uppercase letter. In case of a parametric data type, the type name
continues with a left angle (<
), a list of type names separated by commas
and ends with a right angle (>
).
Type ::= |
TypeIdentifier [ |
New types are defined as either interfaces or algebraic data types. Algebraic data types can be parametric, which is useful for defining “container-like” data types. Currently only algebraic data types can be parameterized, i.e., ABS does not currently offer class templates.
Note that classes are not types in ABS; object references are typed by an
interface that the object’s class implements. (Classes that do not implement
any interfaces can be instantiated, and can interact with the rest of the
system via their run
method and constructor arguments.)
String (1)
A_s1mple_type (2)
Map<Int, List<String>> (3)
1 | The type name is String . The string type is defined in the standard library. |
2 | This is a type name containing underscores and digits. |
3 | This type name denotes a map from integers to lists of strings. |
3.1. Built-in Types
ABS offers the following built-in datatypes:
Name | Description | Example | Documentation |
---|---|---|---|
|
The empty (void) type |
|
|
|
Boolean values |
|
|
|
Integers of arbitrary size |
|
|
|
Rational numbers |
|
|
|
Floating-point numbers |
|
|
|
Strings |
|
|
|
Futures |
— |
|
|
Supertype of all futures |
— |
|
|
Exceptions |
|
|
|
Lists of values of type |
|
|
|
Sets of values of type |
|
|
|
Maps from type |
|
|
|
Pairs of values |
|
|
|
Triples of values |
|
|
|
Optional values |
|
Int
type is a subtype of Rat
; this means that Int
values are assignable
to places of type Rat
. Rational values can be converted to integers via the
truncate
function. Float
is a distinct numeric type.
Support for floating-point calculations is under development;
calculations resulting in Inf or NaN currently have unspecified runtime
behavior.
|
The future type Fut<A>
is a special built-in type that denotes that an ABS
value of type A
will become available in the future. The value that a
future holds and will return can be of any concrete type.
Fut<String> (1)
Fut<List<Rat>> (2)
1 | This future will contain a value of type String |
2 | This future will contain a list of rational numbers |
3.2. Algebraic Data Types
Algebraic Data Types in ABS are used to implement user-defined, immutable data values. Because values of algebraic data types are immutable, they can be safely passed on to other objects and cogs and make it easy to reason about program correctness.
DataTypeDecl ::= |
|
TypeParams ::= |
|
DataConstr ::= |
SimpleTypeIdentifier [ |
Data Type Constructors enumerate the possible values of a data type. Constructors can have zero or more arguments. Each argument can have an optional accessor function (see Accessor Functions).
data IntList = NoInt | ConsInt(Int head, IntList tail); (1)
data Bool = True | False; (2)
data NotInstantiable; (3)
1 | The data type IntList has two constructors: NoInt and ConsInt . The second constructor defines two accessor functions. |
2 | This is the definition of the built-in data type Bool . |
3 | This type does not have constructors and therefore cannot be instantiated. |
Accessor Functions
Data constructor arguments can optionally have a name, which needs to be a valid identifier. If a name is given, it defines a function that, when passed a value expressed with the given constructor, return the argument.
The name of an accessor function must be unique in the module it is defined in. It is an error to have a function definition with the same name as an accessor function or to have multiple accessor functions with the same name, unless they belong to the same data type.
data Person = Person(String name, Int age);
{
Person john = Person("John", 34);
Int age = age(john); (1)
}
1 | The call to age returns 34. |
Parametric Data Types
Algebraic data types can carry type parameters. Data types with type parameters are called parametric data types.
Parametric Data Types are useful to define “container” data types, such as
lists, sets or maps. Parametric data types are declared like normal data types
but have an additional type parameter section inside broken brackets (<
>
)
after the data type name.
data List<A> = Nil | Cons(A, List<A>);
When using a parametric data type, concrete types are given for the type parameters.
List<Int> l = Cons(1, Cons(2, Nil));
N-ary Constructors
Literal values of recursive data types like lists and sets can be arbitrarily long, and nested constructor expressions can become unwieldy. ABS provides a special syntax for n-ary constructors, which are transformed into constructor expressions via a user-supplied function.
def Set<A> set<A>(List<A> l) = (1)
case l {
Nil => EmptySet
| Cons(x,xs) => insertElement(set(xs), x)
};
{
Set<Int> s1 = set(Cons(1, Cons(2, Cons(3, Nil)))); (2)
Set<Int> s = set[1, 2, 3]; (3)
}
1 | The parametric function set is defined to take a list of elements and return a set. |
2 | set is called with a literal list constructed as normal. |
3 | set is called with the special n-ary constructor syntax. The two calls return the same value. |
The constructor function usually has the same name as the type it is
constructing. For example, a value of type Set
is constructed via the
function set
.
Fully Abstract Data Types
Using the module system it is possible to define abstract data types. For an abstract data type, only the functions that operate on them are known to the client, but not its constructors. This can be easily realized in ABS by putting such a data type in its own module and by only exporting the data type and its functions, without exporting the constructors.
3.3. Interface Types
Interfaces in ABS describe the functionality of objects. Thus, Interfaces in ABS are similar to interfaces in Java. Unlike Java, object references are typed by interfaces and not by their class. See Type-Check Expressions and Type-Cast Expressions for how to obtain a reference to the same object via a different interface.
The syntax of interfaces is given in Interfaces.
3.4. Exceptions
In higher-level programming languages, exceptions are generally used to signal an erroneous or abnormal runtime behavior of the program, that should be treated (handled) separately compared to normal values.
Exceptions are declared with the keyword exception
, followed by the name of
an exception and an optional list of parameters. The semantics are the same
as for defining data constructors; naming a parameter will create an accessor
function (see Accessor Functions).
ExceptionDecl ::= |
|
Like any other constructor or datatype name, exceptions always start with an upper-case letter.
Exceptions are of type ABS.StdLib.Exception
, which is pre-defined in the
standard library. It is possible to store exception values in variables of
type Exception
.
exception MyException;
exception MyOtherException(String param, Int); // no accessor for second param
In ABS, exceptions are first-class values; the user can construct
exception-values, assign them to variables, pass them in expressions, etc. An
exception can be thrown via the throw
statement (see Throw)
and be caught in a catch
block (see Handling Exceptions with Try-Catch-Finally).
Additionally, the object itself can recover its invariant after an uncaught
exception in a process via its recovery block (see Classes).
3.5. Type Synonyms
A Type Synonym is an alternative type name for a type. Type synonyms are
introduced with the keyword type
. Parametric type synonyms are not
currently supported.
TypeSynDecl ::= |
|
type Filename = String;
type Filenames = Set<Filename>;
type Servername = String;
type Packet = String;
type File = List<Packet>;
type Catalog = List<Pair<Servername,Filenames>>;
3.6. Location Types
A Location Type is an interface type annotated with a location. The location is seen relative to the current context, and can be one of the following:
Name | Description |
---|---|
|
The object is on a different cog |
|
The object is on the same cog |
|
The object is either |
|
Have the type-checker infer the location |
When location type checking is active, the following example is erroneous, and will result in a type error:
interface I { }
class C {
[Far] I m([Near] I o) {
return o; (1)
}
[Somewhere] I m2([Near] I o) {
return o; (2)
}
}
1 | Here, o is of type [Near] I , which cannot be converted to the return type [Far] I . |
2 | Here, o ’s type [Near] I can be converted to [Somewhere] I . |
4. Expressions
This chapter specifies all ABS expressions. Expressions are usually evaluated for their value, although they can be used inside annotations as purely syntactic constructs that do not need to follow standard evaluation rules.
ABS expressions can either be pure or have side effects. Pure expressions only refer to entities in the functional and expression layer. They can be evaluated multiple times without influencing the execution of the imperative and object oriented layers of ABS. Pure expressions can be sub-expressions of other expressions and can be used as function bodies. Side-effect expressions, on the other hand, refer to semantic entities “above” the functional layer (objects and futures). They are only legal “stand-alone” (i.e., not as a sub-expression of another expression) and in certain places, mostly in the right-hand side of an assignment. This strict separation of pure and side-effect expressions simplifies the language, for example wrt. using expressions inside annotations and reasoning about expressions in static analysis tools.
Exp ::= |
PureExp | EffExp |
PureExp ::= |
SimpleIdentifier |
EffExp ::= |
NewExp |
4.1. Literals
Literals, as defined in Literals, are expressions.
4.2. Template Strings
Template strings are strings allowing embedded expressions. In contrast to normal string literals, template strings can contain linebreaks and other special characters.
A template string starts and ends with a backtick (`
) character. A
template string can contain zero or more pure expressions enclosed by
dollar signs ($
). These expressions will be replaced by their
string representation (as with the function toString
) every time the
template string is evaluated.
The only characters that need to be escaped by a backslash (\
) in a
template string are the backtick (`
) and the dollar sign ($
). All
other characters, including line breaks and the backslash itself, do
not need to be specially treated when writing a template string.
TemplateString ::= |
|
|
`Hello $name$!
The price is \$$price$.`
The result of this expression will be a string with two lines, and with the values of the local variables name
and price
in place.
4.3. Operator Expressions
ABS has a range of unary and binary operators working on pre-defined datatypes. All operators are pure (side effect-free).
OperatorExp ::= |
UnaryExp | BinaryExp |
UnaryExp ::= |
UnaryOp PureExp |
UnaryOp ::= |
|
BinaryExp ::= |
PureExp BinaryOp PureExp |
BinaryOp ::= |
|
The following table describes the meaning as well as the associativity and the precedence of the different operators. The list is sorted from low precedence to high precedence.
Expression | Meaning | Associativity | Argument types | Result type |
---|---|---|---|---|
|
logical or |
left |
|
|
|
logical and |
left |
|
|
|
equality |
left |
compatible |
|
|
inequality |
left |
compatible |
|
|
less than |
left |
compatible |
|
|
less than or equal to |
left |
compatible |
|
|
greater than |
left |
compatible |
|
|
greater than or equal to |
left |
compatible |
|
|
concatenation |
left |
|
|
|
addition |
left |
number |
number |
|
subtraction |
left |
number |
number |
|
multiplication |
left |
number |
number |
|
division |
left |
number |
|
|
modulo |
left |
number |
number |
|
logical negation |
right |
|
|
|
integer negation |
right |
number |
number |
Semantics of Comparison Operators
ABS has generic equality and less-than comparison between two values of the same type.
Equality and inequality comparison is standard: by value for functional
datatypes and by reference for objects and futures. I.e., two strings
"Hello"
compare as identical via ==
, as do two sets containing identical
values. Two references to objects or futures compare as identical via ==
if
they point to the same object or future. The inequality operator !=
evaluates to True
for any two values that compare to False
under ==
and
vice versa.
For the comparison operator <
, an ordering is defined in the following way.
-
Numbers compare as usual.
-
Strings compare lexicographically.
-
Algebraic datatypes compare first by constructor name, then by comparing constructor arguments left to right.
Cons(_, _) < Nil Cons(1, _) < Cons(2, _)
-
Objects and futures are compared by identity, in an implementation-specific but stable way. This means that for any two variables
a
andb
that point to different objects, the value ofa < b
does not change as long asa
andb
are not re-assigned.[1] The valuenull
compares less than any other object reference.
4.4. Let
The expression let T v = p in b
evaluates b
, with v
bound to the value
of evaluating the expression p
. The newly-introduced binding of v
can
shadow a binding of v
outside of the let
expression.
More than one binding can be established in one let
expression.
Bindings are evaluated sequentially, i.e., later bindings can use
earlier variables in their value expression.
LetExp ::= |
|
let Int x = 2 + 2, Int y = x + 1 in y * 2 (1)
1 | The value of this expression is 10 (((2 + 2) + 1) * 2 ) |
4.5. Data Constructor Expressions
Data Constructor Expressions are expressions that create data values by applying arguments to data type constructors. Data constructor expressions look similar to function calls, but data constructors always start with an upper-case letter.
For data type constructors without parameters, the parentheses are optional.
DataConstrExp ::= |
TypeIdentifier [ |
True
Cons(True, Nil)
Nil
Defining new data types and their constructors is described in Algebraic Data Types.
4.6. Function Calls
Function calls apply arguments to functions, producing a value. Function call expressions look similar to data constructor expressions, but function names always start with a lower-case letter. The parentheses are mandatory in function calls.
FnAppExp ::= |
Identifier |
tail(Cons(True, Nil))
head(list)
N-ary Function Calls
Calls to n-ary Constructors (see N-ary Constructors) are written with
brackets ([]
) instead of parentheses (()
).
FnAppListExp ::= |
Identifier |
4.7. Partially-Defined-Function Calls
Calls to partially defined functions (see Partial Function Definitions) are similar to function call expressions, but have an additional prepended set of arguments.
ParFnAppExp ::= |
Identifier |
ParFnAppParam ::= |
Identifier |
AnonymousFunction ::= |
|
map(toString)(list[1, 2])
filter((Int i) => i > 0)(list[0, 1, 2])
4.8. Conditional Expressions
The value of the conditional expression when c then e1 else e2
is either the
value of e1
or the value of e2
, depending on the value of c
, which must
be of type Bool
. Depending on the value of c
, either e1
or e2
is
evaluated, but not both.
WhenExp ::= |
|
when 5 == 4 then True else False
4.9. Case
ABS supports pattern matching via the Case Expression. A case expression consists of an input expression and one or more branches, each consisting of a pattern and a right hand side expression.
The case expression evaluates its input expression and attempts to match the resulting value against the branches until a matching pattern is found. The value of the case expression itself is the value of the expression on the right-hand side of the first matching pattern.
If no pattern matches the expression, a PatternMatchFailException
is thrown.
There are four different kinds of patterns available in ABS:
-
Variables (with different semantics depending on whether the variable is bound or not)
-
Literal Patterns (e.g.,
5
) -
Data Constructor Patterns (e.g.,
Cons(Nil,x)
) -
Underscore Pattern (
_
)
CaseExp ::= |
|
CaseExpBranch ::= |
Pattern |
Pattern ::= |
|
ConstrPattern ::= |
TypeIdentifier [ |
Older versions of the ABS language used the semicolon (’ ) to terminate
case branches. This older syntax is still supported by the compiler.
|
Variable Patterns
Variable patterns are written as identifiers starting with a lower-case letter. If the identifier does not name a variable in the current scope, the variable pattern matches any value and introduces a binding of the given identifier to the matched value for the right-hand side of the branch and the rest of the pattern itself. In case a binding for that identifier is already in scope, its value is compared to the value being matched against.
The variable being named by the variable pattern can be used in the right-hand-side expression of the corresponding branch. Typically, pattern variables are used inside of data constructor patterns to extract values from data constructors. For example:
let (Pair<Int, Int> a) = Pair(5, 5) in
case a {
Pair(x, x) => x (1)
| Pair(x, y) => y (2)
} (3)
1 | This branch matches a pair with identical values. |
2 | This branch matches every pair. Since pairs with identical values are matched by the previous branch, x and y will be different. |
3 | The value of the whole expression is 5, produced by the first branch. |
let (x = 7) in
case Pair(5, 5) {
Pair(x, x) => x (1)
| Pair(x, y) => y (2)
| Pair(y, z) => z (3)
} (4)
1 | This pattern does not match since x is bound to 7 and does not match 5. |
2 | This pattern does not match either, for the same reason. |
3 | This pattern contains only unbound variable patterns and therefore matches. |
4 | The value of the whole expression is 5, produced by the third branch. |
Literal Patterns
Literals can be used as patterns. The pattern matches if the value of the case expression is equal to the literal value.
let (Pair<Int, Int> a) = Pair(5, 5) in
case a {
Pair(3, x) => x (1)
| Pair(x, y) => y (2)
} (3)
1 | The pattern 3 does not match the value in the first position of the Pair constructor pattern. |
2 | This pattern matches. |
3 | The value of the whole expression is 5, produced by the second branch. |
Data Constructor Patterns
A data constructor pattern is written like a standard data constructor expression. Constructor arguments are again patterns.
let (List<Int> l) = list[1, 2, 3] in
case l {
Nil => 0 (1)
| Cons(1, _) => 15 (2)
| Cons(_, Cons(y, _)) => y (3)
} (4)
1 | This pattern matches the empty list. |
2 | This pattern matches a list starting with the literal 1 . |
3 | This pattern matches a list of at least length 2, and binds the second element to y . |
4 | The value of the whole expression is 15, produced by the second branch. |
The Wildcard Pattern
The wildcard pattern, written with an underscore (_
) matches any value.
let (List<Int> l) = list[1, 2, 3] in
case l {
Nil => True (1)
| _ => False (2)
}; (3)
1 | This pattern matches the empty list. |
2 | This pattern matches anything. |
3 | The value of the whole expression is False , produced by the second branch. |
The wildcard pattern can be used as the last pattern in a case expression to define a default case.
A case expression is type-correct if and only if all its expressions and all its branches are type-correct and the right-hand side of all branches have a common super type. This common super type is also the type of the overall case expression. A branch (a pattern and its expression) is type-correct if its pattern and its right-hand side expression are type-correct. A pattern is type-correct if it can match the corresponding case expression.
4.10. Type-Check Expressions
Variables pointing to objects are typed by interface, which means that the concrete class of the referenced object might support more methods than can be called through the reference. The type-check expression checks if an object implements the given interface.
TypeCheckExp ::= |
PureExp |
interface I {}
interface J {}
class C implements I, J {}
{
I o = new C();
if (o implements J) { // evaluates to True
println("o is a J");
}
}
4.11. Type-Cast Expressions
Variables pointing to objects are typed by interface, which means that
the concrete class of the referenced object might support more methods
than can be called through the reference. The type-cast expression
returns a reference of type I
to the same object if it implements
the given interface I
, or null
otherwise.
TypeCastExp ::= |
PureExp |
interface I {}
interface J {}
class C implements I, J {}
class D implements I {}
{
I o = new C();
J j = o as J; // j is an alias of o, with type J
I o2 = new D();
J j2 = o2 as J; // j2 is null
}
4.12. New
A new
expression creates a new object from a class name and a list of
arguments. In ABS objects can be created in two different ways. Either they
are created in the current COG, using the new local
expression, or they are
created in a new COG by using the new
expression (see
The ABS Actor and Concurrency Model for more details about cogs).
This expression is a side-effect expression and cannot be used as a sub-expression. |
NewExp ::= |
|
new local Foo(5)
new Bar()
Classes can declare an init block (see Classes), which is executed for
each new instance. The semantics of the new
expression guarantee that the
init block is fully executed before the new object begins receiving method
calls. Classes can also declare a run
method, which is asynchronously
invoked after the init block and subject to the normal scheduling rules for
processes.
When the fresh object gets passed this as argument, it is
possible to execute synchronous calls to the creating object in the
init block while the creating task is executing the new or new
local expression. This works as expected when the fresh object is on
the same cog (i.e., is created with new local but will deadlock when
the fresh object is on its own cog).
|
4.13. Synchronous Method Calls
A synchronous call consists of a target expression evaluating to an interface type, a method name declared in that interface, and a list of argument expressions.
This expression is a side-effect expression and cannot be used as a sub-expression. |
SyncCall ::= |
PureExp |
Bool b = x.m(5, 3);
The semantics of the synchronous method call differ depending on whether the caller and callee are in the same cog. A synchronous method call between objects in the same cog has Java-like semantics, i.e., the caller is suspended and the called method starts executing immediately. When the called method finishes, the caller process is scheduled and resumes execution.
In the case when caller and called object are in different cogs, a synchronous
method call is equivalent to and asynchronous method call immediately followed
by a get
expression on the resulting future. This means that the intuitive
semantics of synchronous method calls are preserved, but introduces the
possibility of deadlocks in case the callee tries to call back to an object of
the caller cog.
4.14. Asynchronous Method Calls
An asynchronous call consists of a target expression evaluating to an interface type, a method name declared in that interface, and a list of argument expressions.
This expression is a side-effect expression and cannot be used as a sub-expression. |
AsyncCall ::= |
PureExp |
An asynchronous method call creates a new task in the COG that contains the
target. This means that the caller task proceeds independently and in
parallel with the callee task, without waiting for the result. The result of
evaluating an asynchronous method call expression o!m(e)
is a future of
type (Fut<V>
), where V
is the return type of the callee method m
.
This future is resolved (i.e., it gets a value) when the callee task finishes. It can be used to synchronize with the callee task and obtain the result of the method call.
Fut<Bool> f = x!m(5);
4.15. Get
A get expression is used to obtain the value from a future. The current task is blocked until the future has been resolved, i.e., until either the return value is available or an exception has occurred in the callee task. No other task in the COG can be activated while the current task is blocked by a get expression.
This expression is a side-effect expression and cannot be used as a sub-expression. |
GetExp ::= |
PureExp |
Bool b = f.get;
If the future contains a normal return value, the value of the get expression is that value. If the future contains an exception thrown by the callee process, evaluating the get expression will throw the same exception. The value thrown by a get expression can be caught by try-catch as normal (see Handling Exceptions with Try-Catch-Finally).
The following example assigns the return value contained in f
to the
variable b
. In case of any error, b
is assigned False
.
try b = f.get; catch { _ => b = False; }
4.16. Await (Expression)
An await expression is a way to asynchronously call a method, wait for the callee to finish, and optionally get the result in one expression.
This expression is a side-effect expression and cannot be used as a sub-expression. |
AwaitExp ::= |
|
A x = await o!m();
The statement above is equivalent to these three statements:
Fut<A> fx = o!m();
await fx?;
A x = fx.get;
Exception Propagation and the Await Expression
As explained in Section Get, exceptions propagate from callee
to caller via the get
expression. An await
statement will proceed once
the callee process has finished, but an exception in the future will not be
raised when executing the await
statement. To align the await
expression
with that behavior, an exception will only be raised when the return value of
a method call is used, e.g., by assigning it to a variable. Hence the
following line of code will not raise an error even if the call to o!m()
results in an exception:
await o!m();
Since the return value is ignored in the statement above, it is equivalent to these two statements:
Fut<A> fx = o!m();
await fx?;
4.17. Destiny (Expression)
This feature is only available in the Erlang backend. |
The destiny
expression returns the future of the asynchronous call that is
currently being executed.
It returns a value of the Destiny
type. For more information on how Destiny
values can be used, see The Destiny Type.
DestinyExp ::= |
|
class C {
Destiny myMethod() {
return destiny;
}
Unit run() {
Fut<Destiny> f = this!myMethod();
await f?;
Destiny g = f.get;
assert(f == g); (1)
}
}
1 | the boolean expression will evaluate to True since the returned destiny
value is the same as the future of the asynchronous call to myMethod . |
If destiny
is evaluated as part of a synchronous call S then it evaluates to
the future of the task that is executing S:
class C {
Unit syn(Destiny caller) {
assert(destiny == caller);
}
Unit asyn() {
this.syn(destiny);
}
Unit run() {
this!asyn();
}
}
There is also the destinyOf built-in function that can retrieve the
future of the asynchronous call that is currently being executed by a Process ,
see Processes and Process Attributes.
|
5. Function Definitions
Functions take a list of arguments and evaluate the expression in their body, producing a return value. ABS functions are always pure. This means the body of a function can use all pure expressions but no expressions with side effects.
Functions can be parametric, which means that they can take and return
parametric datatypes. This means that a function head
defined over a
parametric list datatype can return the first element of a list, regardless of
its type. Parametric functions are defined like normal functions but have an
additional type parameter section inside angle brackets (<
>
) after the
function name.
FunctionDecl ::= |
|
def Rat abs(Rat x) = if x > 0 then x else -x; (1)
def Int length<A>(List<A> list) = (2)
case list {
Nil => 0
| Cons(_, ls) => 1 + length(ls)
};
def A head<A>(List<A> list) = (3)
case list { Cons(x, _) => x };
1 | The abs function returns the absolute value of its argument. |
2 | This parametric function takes lists with arbitrary values and returns an Integer. |
3 | This parametric function returns the same type that is contained in the list. (Note that head is a partial function which is not defined for empty lists.) |
5.1. Partial Function Definitions
For reasons of simplicity and analyzability, ABS does not offer higher-order
functions. On the other hand, many common patterns of functional programming
are extremely useful, for example the well-known map
, filter
and fold
higher-order functions. For this reason, ABS supports partial function
definitions.
Partial function definitions are function definitions taking an additional set of parameters. These additional parameters can be either names of normal functions, or anonymous functions (see Anonymous Functions). Partial function definitions define a set of functions which only differ in function applications but share overall structure. Put another way, partial function definitions define second-order functions — functions that take first-order functions as arguments. Partially defined functions can be used inside functional code, but cannot be passed as parameters to other partial functions.
A partially defined function is called the same way as a normal function, with a separate, non-empty argument list containing the functional arguments. For recursion inside the body of a partially defined function, omit the function parameter list.
PartialFunctionDecl ::= |
|
// Simply applies a function fn to a value.
def B apply<A, B>(fn)(A value) = fn(value);
def Int double(Int x) = x * 2;
{
// doubled = 4
Int doubled = apply(double)(2);
}
def List<B> map<A, B>(f)(List<A> list) = case list { (1)
Nil => Nil
| Cons(x, xs) => Cons(f(x), map(xs)) (2)
};
def Int double(Int x) = x * 2;
{
// doubled = [2, 4, 6]
List<Int> doubled = map(double)(list[1, 2, 3]);
}
1 | This definition of map is contained in the standard library. |
2 | Note the recursive call to map omits the function parameter list. |
For each call of a partial function, a normal function definition is generated at compile time by replacing the functional parameters syntactically by the functions passed in the additional parameter list. This is done before type checking and after delta and trait flattening — any type mismatch and similar errors are caught afterwards during type checking. If multiple statements call a partially defined function with the same function-name arguments, only one expansion is generated. |
5.2. Anonymous Functions
To reduce the need to declare a function with a new function name explicitly every time a partially defined function is called, ABS uses anonymous functions. Anonymous functions are only allowed in the first arguments list calls to partially defined functions.
AnonymousFunction ::= |
|
An anonymous function specifies a number of parameters and an expression that may refer to the declared parameters.
The following example is equivalent to the previous example, but does not
define the double
function explicitly.
{
List<Int> list = list[1, 2, 3];
list = map((Int y) => y * 2)(list);
}
Anonymous functions can refer to variables and fields accessible in the context of the partial function call. (Since anonymous functions are not first-class values, no closure is created.)
{
Int factor = 5;
List<Int> list = list[1, 2, 3];
list = map((Int y) => y * factor)(list);
// list = [5, 10, 15]
}
Anonymous functions are inlined into the expansion of the partial function definition. Errors caused by wrong typing are caught after the expansion during the type checking of core ABS, but the expanded function definition has an annotation referring to the statement that caused the expansion, hence error reporting will be accurate wrt. the original source code. |
5.3. Built-In Functions
Some special "functions" cannot be defined with pure expressions, for example
the function println
. The definition of such functions is done via a
special function body written as builtin
, with optional pure expression
arguments builtin(a, "b", 3)
. Such builtin functions have to be defined
separately in the code generator for each backend where the model is compiled.
Built-in functions in the standard library include:
-
The functions
sqrt
,log
,exp
that work on floating-point numbers -
Functions that convert between different numerical types:
truncate
,float
,rat
,floor
,ceil
,numerator
,denominator
-
String functions:
substr
,strlen
,toString
,println
-
Clock access:
currentms
,ms_since_model_start
-
Functions that return process attributes (see Section Processes and Process Attributes)
-
The
random
function
5.4. Embedded SQLite Database Queries
The Erlang and Java backends can read from a relational database and convert the result into ABS lists. Currently only SQLite databases are supported.
A SQLite database is queried by writing a function with a builtin
body with
three or more arguments: a literal sqlite3
, the name of the database file,
the query as a SQL string, and zero or more arguments to the query. The
return value is a list of ABS values. The return type of such a function can
be:
-
Int
, when the query returns rows with a single SQLiteINTEGER
value; -
Float
, when the query returns rows with a single SQLiteINTEGER
orREAL
value; -
Rat
, when the query returns rows with a single SQLiteINTEGER
orREAL
value; -
Bool
, when the query returns rows with a single SQLiteINTEGER
value, where0
corresponds to ABSFalse
; -
String
, when the query returns rows with a single SQLiteTEXT
value; -
An algebraic datatype, with a single constructor taking only the above datatypes, such that the constructor can be invoked for each row returned by the query.
Query parameters are written as ?
in the SQL query string. Each of these
parameters must be supplied with a value. ABS query parameters are converted
into SQL values (see https://www.sqlite.org/datatype3.html) as follows:
-
Int
values are converted intoINTEGER
; -
Float
values are converted intoREAL
; -
Rat
values are converted into floating-point numbers before passing them to the query function; -
Bool
values are converted into0
and1
(note that SQLite treats all non-zero values as true; consider directly using a query parameter of typeInt
instead); -
String
values are converted intoSTRING
.
$ sqlite3 /tmp/test.sqlite3
CREATE TABLE IF NOT EXISTS test_table (
int_value INTEGER,
float_value REAL,
string_value TEXT,
bool_value BOOLEAN
);
INSERT INTO test_table(int_value, float_value, string_value, bool_value)
VALUES (15, 13.53, "hello", 0);
INSERT INTO test_table(int_value, float_value, string_value, bool_value)
VALUES (30, 42.5, "world", 1);
.quit
With the above database, the following ABS model can be run:
module Test;
def List<String> fstring() = builtin(sqlite3, (1)
"/tmp/test.sqlite3",
"SELECT string_value FROM test_table");
def List<Rat> frat() = builtin(sqlite3, (2)
"/tmp/test.sqlite3",
"SELECT float_value FROM test_table");
data RowResult = RowResult(Int, Bool, Float, Rat, String);
def List<RowResult> ftuple() = builtin(sqlite3, (3)
"/tmp/test.sqlite3",
`SELECT int_value, bool_value, float_value, float_value, string_value
FROM test_table`);
def List<RowResult> ftuple_with_params(String str, Rat rat) = builtin(sqlite3,
"/tmp/test.sqlite3",
`SELECT int_value, bool_value, float_value, float_value, string_value
FROM test_table
WHERE string_value = ? (4)
AND float_value = ?`,
str, (5)
rat);
{
foreach (v, i in frat()) {
println(`$i$'th rational value is $v$`);
}
foreach (v, i in fstring()) {
println(`$i$'th string value is $v$`);
}
foreach (v, i in ftuple()) {
println(`$i$'th tuple is $v$`);
}
foreach (v, i in ftuple_with_params("world", 85/2)) {
println(`$i$'th tuple is $v$`);
}
}
1 | A builtin function with first argument sqlite3 takes two or more additional arguments: the name of the database, a SQL query string, and the query parameters. |
2 | SQL REAL values are converted to ABS rational values |
3 | SQL query results with more than one column are converted into ABS user-defined datatypes. The data constructor of the result datatype has to take the same number of arguments as the SELECT returns, and the datatypes must be compatible. |
4 | Query parameters in SQLite are written as plain ? in the query string. |
5 | For each parameter in the SQL query, a value must be supplied. |
For the Erlang backend only, when the database file (the second argument
to the builtin expression) does not contain a path, the model will look for
it in the priv directory of the compiled model. That directory is the value
of the erlang function code:priv_dir(absmodel) , typically
gen/erl/absmodel/_build/default/lib/absmodel/priv/ .
|
6. Statements
This chapter specifies all ABS statements.
Statement ::= |
SkipStmt |
6.1. Skip
The skip statement is a statement that does nothing.
SkipStmt ::= |
|
skip;
6.2. Variable Declarations
A variable declaration statement is used to declare variables. Variable declarations can occur at any point in a sequence of statements; i.e., it is not necessary to declare variables only at the beginning of methods or blocks.
Variables declared inside a block are in scope for the duration of the block. It is an error to declare a variable with the same name of another variable in scope. A local variable can have the same name as an object field.
A variable declaration has an expression that defines the initial value of the
variable. The initialization expression is mandatory except for variables of
reference types (interfaces and futures), in which case the variable is
initialized with null
if the initialization expression is omitted.
VarDeclStmt ::= |
Type SimpleIdentifier [ |
Bool b = True;
Constant Declarations
Variable and field declarations can carry a Final
annotation. the effect of
such an annotation is to forbid re-assignment to such variables.
The following example will lead to a compile-time error since we are trying to
assign a new value to constant_i
:
{
[Final] Int constant_i = 24;
constant_i = 25;
}
6.3. Assignment
The assign statement assigns a value to a variable or a field.
Assignments to a field f
can be written either this.f = e;
or f = e;
.
In case a local variable f
is in scope at the point of the assignment
statement, the this
prefix has to be used to assign to the field f
; assignment
to f
will change the value of the local variable.
AssignStmt ::= |
[ |
this.f = True;
x = 5;
6.4. Expressions as Statements
An expression statement is a statement that consists of a single expression. When an expression statement is executed, the expression is evaluated and the resulting value is discarded.
Expression statements are used for their side effects, for example issuing an asynchronous method call without waiting for its result.
ExpStmt ::= |
Exp |
Creating an object without storing a reference (and hence never invoking
a method on the new object) can be a meaningful operation, for example when
the object has a run method and interacts with the rest of the system by
calling methods on references passed in via the new expression.
|
server!operate();
new Client(server);
6.5. Assertions
An assert statement is a statement for asserting certain conditions. If the
expression evaluates to True
, executing an assertion is equivalent to
skip;
. If the expression evaluates to False
, it is equivalent to throw
AssertionFailException;
.
AssertStmt ::= |
|
assert x != null;
6.6. Await (Statement)
An await statement suspends the current task until the given guard becomes
active (evaluates to True
). While the task is suspended, other tasks within
the same COG can be scheduled.
Guards can wait for a futures to become resolved, for a Boolean condition over the object state to become true, or (in timed ABS) for a certain duration to pass.
When evaluating a guard results in an exception, that exception will be thrown when the task is scheduled next, and can be caught as normal (see Handling Exceptions with Try-Catch-Finally). Note that a guard can be evaluated multiple times before its task is scheduled — the most recent exception will be thrown even if the guard evaluated without an exception afterwards. |
In general, each cog will continue running a task without preempting it until
the task is finished or it reaches a scheduling point. Await statements are
scheduling points, as are suspend
statements and assignment or expression
statements containing an await expression (see Await (Expression)).
AwaitStmt ::= |
|
Guard ::= |
[ |
Fut<Bool> f = x!m();
await f?; (1)
await this.x == True; (2)
await f? & this.y > 5; (3)
await duration(3, 5); (4)
1 | A claim guard becomes active when the future is resolved (contains a value or an exception). |
2 | A field guard is a Boolean expression over the object state. |
3 | A guard conjunction becomes active when both its components are active. |
4 | A duration guard becomes active after a certain amount of simulated time has passed. See Timed ABS for more on timed models. |
6.7. Unconditional Release: Suspend
The suspend statement causes the current task to be suspended.
SuspendStmt ::= |
|
There is no guarantee that the cog will choose another task to run; the current task might be resumed immediately after suspending itself. |
It is tempting to misuse the suspend statement for
busy-waiting (while (!condition) suspend; doTheThing(); ). In all
cases, it is better to await on the condition: await condition; doTheThing(); .
|
suspend;
6.8. Return
A return statement returns a value from a method. A return statement can only appear as a last statement in a method body.
For asynchronous method calls, executing the return statement will cause the future to be resolved so that it contains a value. Any claim guards awaiting the future will become active.
Methods that have a Unit
return type do not need an explicit return
statement. The future will be resolved when the method terminates.
ReturnStmt ::= |
|
ABS does not allow exiting a method from multiple points, e.g., via
multiple return statements. This makes model analysis easier.
|
return x;
6.9. Throw
The statement throw
signals an exception (see Exceptions). It
takes a single argument of type ABS.StdLib.Exception
, which is the exception
value to throw.
ThrowStmt ::= |
|
throw AssertionFailException;
Note that the 'throw' statement can only be used inside imperative code.
Functional code that cannot return a value in all cases should use the Maybe
datatype.
def Maybe<Int> f(Int x, Int y) = if (y < 0) then None else Just(x);
Furthermore, note that some built-in exceptions, like
DivisionByZeroException
and PatternMatchFailException
can originate from
functional code. See Predefined exceptions in the Standard Library for a list of built-in
exceptions.
6.10. Blocks of Statements
A sequence of statements is called a block. A block introduces a scope for local variables.
Block ::= |
|
Semantically, a whole block is a single statement and can be written anywhere a single statement is valid. |
{
Int a = 0; (1)
a = a + 1;
n = a % 10;
}
{ } (2)
1 | The variable a is in scope until the end of the block. |
2 | An empty block is equivalent to skip; . |
6.11. Conditionals
ABS has the standard conditional statement. The condition has to evaluate to a Boolean value.
IfStmt ::= |
|
if (5 < x) {
y = 6;
}
else {
y = 7;
}
if (True)
x = 5;
6.12. Switch: Pattern Matching
The switch statement, like the case expression (see Case), consists of an expression and a series of branches, each consisting of a pattern and a statement (which can be a block).
When a switch statement is executed, its input expression is evaluated and the value matched against the branches until a matching pattern is found. The statement in the right-hand side of that branch is then executed. Any variable bindings introduced by matching the pattern are in effect while executing that statement.
If no pattern matches the expression, a PatternMatchFailException
is thrown.
For a description of the pattern syntax, see Case.
SwitchStmt ::= |
|
SwitchStmtBranch ::= |
Pattern |
Pair<Int, Int> p = Pair(2, 3);
Int x = 0;
switch (p) {
Pair(2, y) => { x = y; skip; }
_ => x = -1;
}
6.13. The While Loop
The while loop repeats its body while the condition evaluates to True
. The
condition is re-evaluated after each iteration of the loop.
WhileStmt ::= |
|
while (x < 5) {
x = x + 1;
}
6.14. The Foreach Loop
The foreach loop repeatedly executes its body with a loop value variable bound to each element of the given list, in sequence. An optional loop index variable is bound to the index of the element bound to the value variable, starting with 0.
The rules for the loop variables follow the rules of local variable declarations in blocks: the loop variables cannot shadow existing variables, but can use the same name as an object field. Loop variables go out of scope after execution leaves the body of their foreach loop.
Also see the second-order functions map , filter , foldl and foldr
in Section Lists for common list processing idioms.
|
ForeachStmt ::= |
|
foreach (v in list["a", "b", "c"]) {
println(`v is bound to $v$`);
}
foreach (v, i in list["a", "b", "c"]) {
println(`Element number $i$ has value $v$`);
}
6.15. Handling Exceptions with Try-Catch-Finally
Executing a statement can result in an exception, either explicitly signaled
using the throw
keyword or implicitly, for example by dividing by zero. The
try-catch-finally statement is used to handle exceptions and resume normal
execution afterwards.
TryCatchFinallyStmt ::= |
|
The statement protected by try
(which can be a block) is executed first. If
no exception is thrown, execution continues with the optional finally
statement, then with the next statement after the try-catch-finally statement.
If during execution of the statement protected by try
an exception is
thrown, it is matched one-by-one against the exception patterns defined in the
catch
block. The statement following the first matching pattern will be
executed, as in the switch statement (see Switch: Pattern Matching). Execution continues
with the optional finally
statement, then with the statement following the
try-catch-finally statement.
If during execution of the statement protected by try
an exception is thrown
that is not matched by any branch in the catch
block, the exception is
unhandled. In this case, first the optional finally
statement is
executed. If the try-catch-finally was protected by another try-catch-finally
statement, the unhandled exception is passed on to this surrounding
try-catch-finally statement. Otherwise, the current process terminates and
its future is resolved by storing the unhandled exception. Any get
expression on this future will re-throw the exception (see
Get). The object that ran the aborted process will execute its
recovery block with the unhandled exception as parameter (see
Classes).
try {
Rat z = 1/x; (1)
} catch {
DivisionByZeroException => println("We divided by zero"); (2)
} finally {
println("Leaving the protected area"); (3)
}
1 | If x is zero, this will throw an exception |
2 | Division by zero is handled here; other exceptions will be left unhandled |
3 | This statement is always executed |
As a syntactic convenience, when matching only a single pattern, the braces around the catch block can be omitted.
try b = f.get; catch _ => b = False; (1)
1 | A “catch-all” exception handler that sets b to a default value in case an unhandled exception was propagated via f |
The finally block has the same restrictions as the class init and
recovery blocks, i.e., it cannot contain processor release points (i.e.,
await or suspend ), blocking expressions (i.e., get ), or explicitly throw
an exception via the throw statement.
|
7. Interfaces
Interfaces in ABS are similar to interfaces in Java. They have a name, which
defines a nominal type, and they can extend one or more interfaces. If no
explicit extends
clause is given, the interface will extend the standard
library interface ABS.StdLib.Object
(see Object). The
ABS.StdLib.Object
interface does not specify any methods.
The interface body consists of a list of method signature declarations. Method names start with a lowercase letter.
InterfaceDecl ::= |
|
InterfaceList ::= |
TypeIdentifier { |
MethSig ::= |
Type SimpleIdentifier |
If a method declarations in an interface has an [Atomic]
annotation, the
compiler will statically check that any definitions for this method contain no
suspension points (suspend
and await
statements), i.e., that running that
method will complete without scheduling.
If a method declarations in an interface has a [Readonly]
annotation, the
compiler will statically check that any definitions for this method contain no
assignments to fields.
For ease of reasoning and analysis, ABS methods differ only by name. It is an error for an interface to declare two methods with the same name, either explicitly or via extending another interface. |
The interfaces in the example below represent a database system, providing functionality to store and retrieve files, and a node of a peer-to-peer file sharing system. Each node of a peer-to-peer system plays both the role of a server and a client.
interface DB {
File getFile(Filename fId);
Int getLength(Filename fId);
Unit storeFile(Filename fId, File file);
Filenames listFiles();
}
interface Client {
List<Pair<Server,Filenames>> availFiles(List<Server> sList);
Unit reqFile(Server sId, Filename fId);
}
interface Server {
Filenames inquire();
Int getLength(Filename fId);
Packet getPack(Filename fId, Int pNbr);
}
interface Peer extends Client, Server {
List<Server> getNeighbors();
}
8. Classes
Classes in ABS describe stateful behavior. All objects in ABS have exactly
one class; objects are created via the new
expression.
Classes implement one or more interfaces. If a class implements more than
one interface, Type-Check Expressions and Type-Cast Expressions are used
to obtain a reference to the same object typed with a different interface. If
no explicit implements
clause is given, the class will extend the standard
library interface ABS.StdLib.Object
(see Object). The
ABS.StdLib.Object
interface does not specify any methods.
Classes typically explicitly implement one or more interfaces so that
methods can be called on them, but the run method makes it meaningful to
have objects without public methods; such objects cannot accept method calls
but can send method calls to other objects.
|
For ease of reasoning and analysis, ABS methods differ only by name. It is an error for a class to implement two interfaces that both contain a method with the same name. |
Classes have fields that define the state of objects of that class. All fields are private and can only be accessed from methods defined in the class. Fields are defined in two ways:
-
Field declarations in the body of the class define fields that get their initial value from their init expression.
-
Class parameters define fields that get their initial value from the
new
expression.
Classes have an optional init block, which is executed for each new object
before any other code. The init block cannot contain processor release points
(i.e., await
or suspend
), blocking expressions (i.e., get
, duration
,
using resources), or explicitly throw an exception via the throw
statement.
It is mainly used for complicated field initializations and is frequently
omitted.
Classes have an optional run method (see Active Classes).
Classes have an optional recovery block. In case an uncaught exception
occurs in a method, the exception is matched against the patterns given in the
recovery block, and the associated statement(s) are executed. If the
exception does not match any pattern in the recovery block, or if the recovery
block itself raises an exception, the object is killed. Code in the recovery
block has the same restrictions as in the init block (i.e., no processor
release points, blocking expressions and throw
statements).
ClassDecl ::= |
|
ClassParameterList ::= |
Type SimpleIdentifier { |
InterfaceList ::= |
TypeIdentifier { |
TraitUse ::= |
adds TraitExpr |
FieldDeclList ::= |
{ Type SimpleIdentifier [ |
RecoveryBlock ::= |
|
MethDecl ::= |
Type SimpleIdentifier |
A class definition contains zero or more method definitions. Each method has a name, return type and zero or more parameters. All methods declared in an interface that is implemented by the class or one of their super-interfaces must be defined in the class body or in one of the used traits (see Traits). A class is free to define methods not declared in an interface; such methods are private to the class and cannot be called from outside the class.
ABS currently does not support method overloading. Each method must have a unique name since methods are not disambiguated by their parameter lists. |
class DataBase(Map<Filename,File> db) implements DB {
File getFile(Filename fId) {
return lookup(db, fId);
}
Int getLength(Filename fId){
return length(lookup(db, fId));
}
Unit storeFile(Filename fId, File file) {
db = insert(Pair(fId,file), db);
}
Filenames listFiles() {
return keys(db);
}
}
class Node(DB db, Peer admin, Filename file) implements Peer {
Catalog catalog;
List<Server> myNeighbors;
// implementation...
}
8.1. Constant Fields
Similar to variable declarations, field declarations and class parameters can
carry a Final
annotation. the effect of such an annotation is to forbid
re-assignment to such a field.
The following example will lead to compile-time errors since we are trying to
assign new values to two fields declared as Final
:
class Sample ([Final] Int constant_i) {
[Final] Int constant_j = 24;
Unit m() {
constant_i = 25; // error
constant_j = 24; // error
}
}
In addition to fields, method parameters and variables can also be declared
Final
.
8.2. Read-only Methods
For any method definition annotated with [Readonly]
, the compiler will
statically check that its body does not contain assignments to fields.
If an interface declares a method to be read-only, its definition has to be
annotated with [Readonly]
as well.
The following example shows a class declaration with an atomic method that will lead to a compile error.
class Wrong {
Int field = 12;
// This will not compile
[Readonly] Unit fails() {
field = field + 1;
}
}
8.3. Atomic Methods
For any method definition annotated with [Atomic]
, the compiler will
statically check that its body does not contain suspension points
(suspend
and await
statements) and blocking get
expressions. Such
methods can be called inside init blocks and in finally
clauses; all other
methods cannot be called in these places.
If an interface declares a method to be atomic, its definition has to be
annotated with [Atomic]
as well.
The following example shows a call to an atomic method from an init block.
Removing the Atomic
annotation from method m
would lead to a compile-time
error.
class Sample {
Int field = 12;
{
field = this.m();
}
[Atomic] Int m() {
return 24;
}
}
8.4. Active Classes
A class can be active or passive. Active classes start an activity on their own upon creation. Passive classes only react to incoming method calls. A class is active if and only if it has a run method:
Unit run() {
// active behavior ...
}
The run method is asynchronously called after object initialization.
9. Traits
ABS does not support inheritance for code reuse. Method implementations that
are common between classes can be defined once and used inside these classes
by using traits. A trait is a set of method definitions that can be added
to a class via the uses
clause in the class definition.
Traits are applied to classes at compile-time and do not change the interface(s) of a class. Classes and their methods are type-checked once all traits are applied.
Traits can re-use and modify other traits via trait operations. A trait can add methods to another trait, modify methods in another trait, or remove methods from another trait.
TraitDecl ::= |
|
TraitExpr ::= |
MethodSet { TraitOper } |
MethodSet ::= |
TraitName | MethDecl | |
TraitName ::= |
SimpleIdentifier |
TraitOper ::= |
|
A trait is defined with trait
t = T at module level.
The effect of applying a trait T to a class (using uses
T inside the
class body) is to add the methods in that trait to the class
definition.
-
The trait operation
adds
adds all the elements of its MethodSet to the trait. It is an error if any method of the MethodSet is already present in the trait. Any error will be raised after applying all operations, during type checking.
trait T = { Unit x(){ skip; } }
trait T2 = { Unit y(){ skip; } } adds T
// T2 contains the following methods:
// Unit y(){ skip; } Unit x(){ skip; }
-
The trait operation
modifies
changes all the methods in the trait to the new implementation described in this MethodSet. If a method with the same name is not present in the trait, the method is added instead.
The methods in the modifies
operation may contain original()
calls which
will call the version of the method before the operation application. In case
an overriden method is not present, original()
calls will lead to a
compile-time error.
trait T = { Bool m(){ return False; } }
trait T2 = { Bool m() { Bool orig = original(); return !orig; } }
class C {
uses T modifies T2;
}
// Calling C.m() will return True
-
The operation
removes
removes the method(s) with the provided signatures from the trait. If a method with the same name is not present in the class (or set of methods), an error will be raised during compilation.
trait T = { Bool m(){ return False; } }
class C {
uses T removes { Bool m(); };
}
// class C does not contain any methods
10. Modules
All ABS definitions (classes, interfaces, functions data types, type aliases) are contained in modules. All definitions are visible in their own module only, except when the module exports the name and it is imported in another module or referenced by its qualified name.
The export clause in a module definition exports names, not
definitions as such. This means that if a module defines a class and an
interface with the same name, both definitions will be accessible if that name
is contained in the export clause.
|
10.1. Defining a Module
ModuleDecl ::= |
|
ExportImport ::= |
Export | Import |
Export ::= |
|
Import ::= |
|
IdentifierList ::= |
AnyIdentifier { |
AnyIdentifier ::= |
Identifier | TypeIdentifier |
Decl ::= |
FunctionDecl |
A module name is a type name and must always start with an upper case letter.
Every module starts with a declaration of the form
module MyModule;
This declaration starts a new module named MyModule
. All declarations after
this line until the next module declaration belong to the module MyModule
.
The module ABS.StdLib contains the standard library and is imported by
every module by default. If a module contains an explicit import x, y from
ABS.StdLib; import clause, only x and y will be imported, otherwise all
names exported from the standard library will be imported in that module.
|
10.2. Exporting Identifiers
By default, modules do not export any names. In order to make names defined within a module available to other modules, the names have to be exported. For example, to export a data type and a data constructor, one can write something like this:
module Drinks;
export Drink, pourMilk, pourWater;
data Drink = Milk | Water;
def Drink pourMilk() = Milk;
def Drink pourWater() = Water;
Note that in this example, the data constructors are not exported, and other
modules can only create values of type Drink
by calling the exported
constructor functions pourMilk
and pourWater
. By only exporting the data
type without any of its constructors, one can realize abstract data types
in ABS.
A special export clause export *;
exports all names that are defined in the
module. Note that imported names are not re-exported by export *;
or
export Name;
(but can be re-exported via export * from OtherModule;
and
export Name from OtherModule;
clauses).
module Test;
export *;
import * from OtherModule;
export * from OtherModule;
10.3. Importing Identifiers
In order to use exported names of a module in another module, the names have to be imported. In a module definition, a list of import clauses follows the list of export clauses. After being imported, these names are accessible in the current module.
Names can be accessible either qualified (with package prefix) or unqualified, depending on how they are imported.
The following example makes the Drink
data type of the module Drinks
accessible as Drinks.Drink
:
module Bar;
import Drinks.Drink; (1)
import pourMilk from Drinks; (2)
1 | The name Drink is accessible in module Bar as Drinks.Drink . |
2 | The name pourMilk is accessible in module Bar both as Drinks.pourMilk and pourMilk . |
The import * from Module;
statement makes all names that are exported from
module Module
accessible without module qualifier in the current module.
module Bar;
import * from Drinks; (1)
1 | All names from Drinks are accessible in Bar with and without the Drinks. prefix. |
Re-exporting Imported Names
It is possible to re-export names that are imported from another module. For example,
module Bar;
import * from Drinks;
export * from Drinks;
re-exports from Bar
all names that are exported by module Drinks
. Another
module that writes import * from Bar;
will have the names from Drinks
accessible as well.
To re-export only selected names, include only these names in the export list:
module Bar;
import * from Drinks;
export Drink from Drinks; (1)
Only Drink
is exported from Bar
.
Note that only names that have been imported can be re-exported. For example:
module Bar;
import Drink from Drinks;
export * from Drinks;
only re-exports Drink
, as this is the only name imported from module
Drinks
.
11. The Standard Library
This chapter documents the ABS standard library. All definitions, except
where noted otherwise, are contained in the module ABS.StdLib
which is
included by default in every module. Therefore, no import
statement is
necessary to use these definitions.
11.1. Boolean values
Datatypes and Constructors
The literals for Boolean values are True
and False
. The name of the
datatype is Bool
.
Bool value = True;
Operators
The following operators apply to Boolean values:
Expression | Meaning | Associativity | Argument types | Result type |
---|---|---|---|---|
e1 || e2 |
logical or |
left |
Bool, Bool |
Bool |
e1 && e2 |
logical and |
left |
Bool, Bool |
Bool |
e1 == e2 |
equality |
left |
compatible |
Bool |
e1 != e2 |
inequality |
left |
compatible |
Bool |
e1 < e2 |
less than |
left |
compatible |
Bool |
e1 <= e2 |
less than or equal to |
left |
compatible |
Bool |
e1 > e2 |
greater than |
left |
compatible |
Bool |
e1 >= e2 |
greater than or equal to |
left |
compatible |
Bool |
! e |
logical negation |
right |
Bool |
Bool |
11.2. Numbers
Datatypes and constructors
The numeric datatypes of ABS are Int
(arbitrary-length integers), Rat
(arbitrary-precision rational numbers) and Float
(64-bit floating point).
See Built-in Types for their syntax.
Support for floating-point calculations is under development;
calculations resulting in Inf or NaN currently have unspecified runtime
behavior.
|
Operators
Expression | Meaning | Associativity | Argument types | Result type |
---|---|---|---|---|
|
equality |
left |
compatible |
|
|
inequality |
left |
compatible |
|
|
less than |
left |
compatible |
|
|
less than or equal to |
left |
compatible |
|
|
greater than |
left |
compatible |
|
|
greater than or equal to |
left |
compatible |
|
|
addition |
left |
number, number |
number |
|
subtraction |
left |
number, number |
number |
|
multiplication |
left |
number, number |
number |
|
division |
left |
number, number |
|
|
modulo |
left |
number, number |
number |
Functions
These functions calculate the maximum and minimum of their two arguments. Since ABS datatypes are ordered, they can be applied to arguments of all types.
A max<A>(A a, A b)
A min<A>(A a, A b)
This function calculates the absolute (positive) value of its argument.
Rat abs(Rat x)
Converts a rational number to an integer by truncating towards zero.
Int truncate(Rat a)
Converts an integer or rational number into a floating-point number.
Float float(Rat a)
Very large integers and some rational numbers cannot be converted
exactly into floating-point numbers. More in general, rat(float(x)) == x
might not be true.
|
Converts a floating-point number into a rational number.
Rat rat(Float f)
Conversion from floating-point to rational numbers is inexact and
backend-specific. In general, float(rat(x)) == x might not be true.
|
Returns the largest integer smaller or equal to the argument.
Int floor(Float f)
Returns the smallest integer larger or equal to the argument.
Int ceil(Float f)
Returns the numerator of a rational number, or the number itself for an integer.
Int numerator(Rat a)
Returns the denominator of a rational number, or 1
for an integer.
Int denominator(Rat a)
This function calculates b
to the power of n
.
Rat pow(Rat b, Int n)
This function approximates the square root of x
; it stops when two subsequent
estimates (as per Newton’s algorithm) differ by less than epsilon
. estimate
is an initial estimate of the
square root.
Rat sqrt_newton(Rat x, Rat estimate, Rat epsilon)
This function approximates e to the power of x
; it stops when two subsequent
estimates (as per Newton’s algorithm) differ by less than epsilon
.
Rat exp_newton(Rat x, Rat epsilon)
This function returns the square root of x
. It is an error if x
is
negative.
Float sqrt(Float x)
This function returns the natural logarithm of its argument.
Float log(Float x)
This function returns Euler’s number e raised to the power of x
.
Float exp(Float x)
Returns an integer between 0 (inclusive) and its argument (exclusive).
Int random(Int below)
11.3. Strings
Datatypes and Constructors
The datatype for strings is String
.
String literals are enclosed in double quotes ("
). Line feed in a string
literal is written as \n
, carriage return as \r
.
Operators
Expression | Meaning | Associativity | Argument types | Result type |
---|---|---|---|---|
e1 == e2 |
equality |
left |
compatible |
Bool |
e1 != e2 |
inequality |
left |
compatible |
Bool |
e1 < e2 |
less than |
left |
compatible |
Bool |
e1 <= e2 |
less than or equal to |
left |
compatible |
Bool |
e1 > e2 |
greater than |
left |
compatible |
Bool |
e1 >= e2 |
greater than or equal to |
left |
compatible |
Bool |
e1 + e2 |
concatenation |
left |
String, String |
String |
Functions
This function converts any data into a printable string representation.
def String toString<T>(T t)
Returns a substring of a given string str
with length length
starting from
position start
(inclusive). The first character in a string has position 0.
def String substr(String str, Int start, Int length)
Returns the length of the given string str
. The empty string (""
) has
length 0.
def Int strlen(String str)
Prints the given string s
to standard output, followed by a newline, meaning
that the next output will not continue on the same line.
def Unit println(String s)
Prints the given string s
to standard output. Does not cause the next
output to begin on a new line.
def Unit print(String s)
11.4. Unit
Unit
is the empty (void) datatype.
Datatypes and Constructors
Both the datatype and the single constructor are named Unit
.
11.5. Object
Object
is the standard super-interface. All interfaces extend this type.
All object references can be assigned to variables of this type.
The Object
interface does not specify any methods.
11.6. The Future Type
Futures are placeholders for return values of asynchronous methods calls.
Future values are produced by asynchronous method calls (see Asynchronous Method Calls). The current process can suspend itself until a future is resolved, i.e., until the return value of the asynchronous method call is available (see Await (Statement)). The get expression returns the value of a future (see Get). In case the future is not yet resolved, the get expression blocks the current cog.
Fut<Int> f = o!add(2, 3); (1)
await f?; (2)
Int result = f.get; (3)
1 | This statement defines a future variable f to hold the integer result of the method call to add . |
2 | The await statement suspends the current process until f is resolved. |
3 | The get expression returns the value computed by the add call. |
Futures are first-class values that can be stored and passed around. In case only the return value of the method call is needed and not the future itself, a shorthand can be used that combines the above three statements:
Int result = await o!add(2, 3); (1)
1 | This statement invokes add , suspends the current process until the result is available, then stores it in result . |
The Destiny Type
This feature is only available in the Erlang backend. |
The Destiny
type is the type of the destiny
expression, see
Destiny (Expression).
Thus, its values are also futures:
Unit myMethod() {
Destiny x = destiny;
}
The Destiny
type is a supertype of all future types.
For example, values of any future type Fut<T>
can implicitly be cast to the
Destiny
type:
Destiny f = this!myMethod();
The Destiny
type supports all boolean comparisons that Fut<T>
types
support. Thus, one can use it to compare futures even across different future
types to identify asynchronous method calls:
Destiny f = this!myMethod();
Fut<Int> g = this!myMethod();
assert(f == f && f != g);
The Get-Expression of a Destiny
value is typed by the internal type Bottom
that has no values.
Thus, it is permitted to synchronize on a future typed by Destiny
but one can
not extract its result:
Int myMethod() {
Destiny f = destiny;
f.get; (1)
await f?; (2)
Int x = f.get; (3)
return 42;
}
1 | This is permitted but results in a deadlock, as the method is now synchronizing on its own completion. |
2 | Applying await to a Destiny value is also permitted but in this case it has the same deadlock problem. |
3 | This will result in a compile-time error. |
11.7. Predefined exceptions in the Standard Library
ABS provides pre-defined exceptions that are thrown in specific circumstances. See Exceptions for information about exceptions.
This list is subject to revision in future versions of ABS. Not all these exceptions are currently thrown by different backends in the described situation. |
- DivisionByZeroException
-
Raised in arithmetic expressions when the divisor (denominator) is equal to 0, as in 3/0
- AssertionFailException
-
The assert keyword was called with False as argument
- PatternMatchFailException
-
The pattern matching was not complete. In other words all c catch-all clause
- NullPointerException
-
A method was called on
null
- StackOverflowException
-
The calling stack has reached its limit (system error)
- HeapOverflowException
-
The memory heap is full (system error)
- KeyboardInterruptException
-
The user pressed a key sequence to interrupt the running ABS program
- ObjectDeadException
-
A method was called on a dead (crashed) object
11.8. Lists
A list is a sequence of values of the same type. Lists are constructed via
the list
constructor function, e.g., list[1, 2, 3]
creates a list of three
integers. An empty list is created via list[]
or Nil
.
The time to access a value via nth
is proportional to the length of the
list. The first value of a list can be accessed in constant time, using the
head
function.
The map
, fold
and filter
second-order functions described in
this section implement common functional programming patterns. To
execute some statements for each element in a list, use a foreach
loop (see The Foreach Loop).
Datatypes and Constructors
A list is defined either as the empty list (Nil
) or as a value a
followed
by another list l
(Cons(a, l)
).
data List<A> = Nil | Cons(A head, List<A> tail);
Literal lists of arbitrary length can be written using a special function
list
. In the following example, l1
and l2
contain the same elements.
List<Int> l1 = list[1, 2, 3];
List<Int> l2 = Cons(1, Cons(2, Cons(3, Nil)));
Functions
Returns the head of a list.
def A head(List<A> l);
Returns the tail (rest) of a list.
def List<A> tail(List<A> l);
Returns the length of a list. The length of Nil
is 0.
def Int length(List<A> l);
Checks if a list is empty. Returns True
for Nil
, False
otherwise.
def Bool isEmpty(List<A> l);
Returns the n
-th element of a list. Returns the head of l
for n
=0,
returns the last element of l
for n
=length(l)-1
.
It is an error if n
is equal to or larger than length(l)
.
def A nth(List<A> l, Int n);
Returns a fresh list where all occurrences of a
have been removed.
def List<A> without<A>(List<A> list, A a);
Returns a list containing all elements of list list1
followed by all
elements of list list2
.
def List<A> concatenate<A>(List<A> list1, List<A> list2);
Returns a list containing all elements of list l
followed by the element p
in the last position.
def List<A> appendright<A>(List<A> l, A p);
Returns a list containing all elements of l
in reverse order.
def List<A> reverse<A>(List<A> l);
Returns a list of length n
containing p
n times.
def List<A> copy<A>(A p, Int n);
Applies a function to each element of a list, returning a list of results in
the same order. The function fn
must take an argument of type A
and
return a value of type B
.
def List<B> map<A, B>(fn)(List<A> l);
Returns a list containing only the elements in the given list for which the
given predicate returns True
. The function predicate
must take an
argument of type T
and return a Boolean value.
def List<T> filter<T>(predicate)(List<T> l);
Accumulates a value starting with init
and applying accumulate
from left
to right to current accumulator value and each element. The function
accumulate
must take two arguments: the first of type A
(the type of the
list) and the second of type B
(the accumulator and result type), and return
a value of type B
.
def B foldl<A, B>(accumulate)(List<A> l, B init);
Accumulates a value starting with init
and applying accumulate
from right
to left to each element and current accumulator value. The function
accumulate
must take two arguments: the first of type A
(the type of the
list) and the second of type B
(the accumulator and result type), and return
a value of type B
.
def B foldr<A, B>(accumulate)(List<A> l, B init);
11.9. Sets
A set contains elements of the same type, without duplicates. Sets are
constructed via the set
constructor function, e.g., set[1, 2, 2, 3]
creates a set of three integers 1, 2, 3. The expression set[]
produces the
empty set.
To add an element to a set, use the function insertElement
, to remove an
element, use remove
. To test for set membership, use the function
contains
.
The takeMaybe
function can be used to iterate through a set. It is used as follows:
def Unit printAll<A>(Set<A> set) =
case takeMaybe(set) {
Nothing => println("Finished")
| Just(e) => let (Unit dummy) = println("Element " + toString(e)) in printAll(remove(set, e))
};
Datatypes and Constructors
The datatype for sets with elements of type A
is Set<A>
. The set
constructor function is used to construct sets.
Functions
Returns True
if set ss
contains element e
, False
otherwise.
def Bool contains<A>(Set<A> ss, A e);
Returns True
if set xs
is empty, False
otherwise.
def Bool emptySet<A>(Set<A> ss);
Returns the number of elements in set xs
.
def Int size<A>(Set<A> xs);
Returns a list with all elements in set xs
.
def List<A> elements<A>(Set<A> xs);
Returns a set containing all elements of sets set1
and set2
.
def Set<A> union<A>(Set<A> set1, Set<A> set2);
Returns a set containing all elements that are present in both sets set1
and
set2
.
def Set<A> intersection<A>(Set<A> set1, Set<A> set2);
Returns a set containing all elements of set set1
not present in set set2
.
def Set<A> difference<A>(Set<A> set1, Set<A> set2);
Returns True
if set
contains all elements of maybe_subset
, False
otherwise.
def Bool isSubset<A>(Set<A> maybe_subset, Set<A> set);
Returns a set with all elements of set xs
plus element e
. Returns a set
with the same elements as xs
if xs
already contains e
.
def Set<A> insertElement<A>(Set<A> xs, A e);
Returns a set with all elements of set xs
except element e
. Returns a set
with the same elements as xs
if xs
did not contain e
.
def Set<A> remove<A>(Set<A> xs, A e);
Returns one element from a non-empty set. It is an error to call take
on an
empty set; consider using takeMaybe
in that case.
def A take<A>(Set<A> ss);
Returns one element from a set, or Nothing
for an empty set.
def Maybe<A> takeMaybe<A>(Set<A> ss);
11.10. Maps
Maps are dictionaries storing a value for each key.
Maps are constructed using by passing a list of type Pair<A, B>
to the map
constructor function. The keys of the resulting map are of type A
and
values are of type B
. The expression map[]
produces an empty map.
The following example produces a map with two entries 1 → "ABS"
and 3 →
"SACO"
.
Map<Int, String> m = map[Pair(1, "ABS"), Pair(3, "SACO")];
In case of duplicate keys, it is unspecified which value the map will contain for a given key. |
The value associated with a key can be obtained using the lookup
and
lookupDefault
functions.
A map can be iterated over via the functions keys
, values
and entries
,
which return the set of keys and the list of values and entries of the map,
respectively.
Datatypes and Constructors
The datatype for a map from type A
to type B
is is Map<A, B>
. The map
constructor function is used to construct maps.
Functions
Returns True
if the map is empty, False
otherwise.
def Bool emptyMap<A, B>(Map<A, B> map);
Returns a map with the first occurrence of key
removed.
def Map<A, B> removeKey<A, B>(Map<A, B> map, A key);
Returns a list of all values within the map.
def List<B> values<A, B>(Map<A, B> map);
Returns a set of all keys of the map.
def Set<A> keys<A, B>(Map<A, B> map);
Returns a list of all entries (i.e., pairs of key and value) of the map.
def List<Pair<A, B>> entries<A, B>(Map<A, B> map);
If value v
is associated with a given key k
, return Just(v)
. Otherwise,
return Nothing
.
def Maybe<B> lookup<A, B>(Map<A, B> ms, A k);
Returns the value associated with key k
. If the map does not contain an
entry with key k
, return the value d
.
def B lookupDefault<A, B>(Map<A, B> ms, A k, B d);
If you need to know whether the map contains an entry for key k , use the
function lookup instead.
|
Returns the value associated with key k
. It is an error if the map does not
contain an entry with key k
.
def B lookupUnsafe<A, B>(Map<A, B> ms, A k);
If there is an entry k,v
in the map, return its key Just(k)
.
Otherwise, return Nothing
.
def Maybe<A> lookupReverse<A, B>(Map<A, B> ms, B v);
If there is an entry k,v
in the map, return its key Just(k)
.
Otherwise, return the value d
.
def A lookupReverseDefault<A, B>(Map<A, B> ms, B v, A d);
If you need to know whether the map contains an entry with value
v , use the function lookupReverse instead.
|
Returns a map with all entries of map
plus an entry p
, which is given as a
pair (Pair(key, value)
) and maps key
to value
. If map
already
contains an entry with the same key key
, it is not removed from the map but
lookup
will return the new value value
. (The function removeKey
removes
the first entry for a given key and thus “undoes” the effect of calling
insert
.)
def Map<A, B> insert<A, B>(Map<A, B> map, Pair<A, B> p);
Returns a map with all entries of ms
plus an entry mapping k
to v
, minus
the first entry already mapping k
to a value.
def Map<A, B> put<A, B>(Map<A, B> ms, A k, B v);
11.11. Pairs
Datatypes and Constructors
The Pair<A, B>
datatype holds a pair of values of types A
and B
,
respectively. The constructor is called Pair
as well.
Pair<Int, String> pair = Pair(15, "Hello World");
Functions
The function fst
returns the first value in a pair.
The function snd
returns the second value in a pair.
11.12. Triples
Datatypes and Constructors
The Triple<A, B, C>
datatype holds a triple of values of types A
, B
and
C
, respectively. The constructor is called Triple
as well.
Triple<Int, String, Bool> triple = Triple(15, "Hello World", False);
Functions
The function fstT
returns the first value in a triple.
The function sndT
returns the second value in a triple.
The function trdT
returns the third value in a triple.
11.13. Optionals
Datatypes and Constructors
The datatype Maybe<A>
wraps a concrete value of type A. The value Nothing
denotes the absence of such a value.
Maybe<Int> answer = Just(42);
Maybe<String> question = Nothing;
Functions
The function isJust
returns False
if the Maybe
value is Nothing
,
True
otherwise.
def Bool isJust<A>(Maybe<A> a);
The function fromJust
returns the wrapped value of a Maybe
. It is an error to call fromJust
on Nothing
.
def A fromJust<A>(Maybe<A> m);
The function fromJustDefault
returns the wrapped value of the first
argument, or the second argument if the first argument is Nothing
.
def A fromJustDefault<A>(Maybe<A> m, A default);
11.14. Others
This subsection lists definitions of the standard library that do not fit in any other sections.
Functions
The function ms_since_model_start
returns a non-negative integer containing the number of milliseconds elapsed since the model was started. It is useful mainly for benchmarking.
12. The Model API
The Erlang backend supports querying a running model. When creating objects,
an annotation HTTPName
makes them available via HTTP requests from outside
the model under the name given by the annotation. Methods that are annotated
HTTPCallable
can be invoked from the outside on such objects. Additionally,
datatypes can be annotated to influence how they are converted to JSON when
they are returned from such methods.
The model API is supported by the Erlang backend.
The Model API might accept connections only from local (loopback) addresses, i.e., from the same machine. |
All queries in this section can be prepended with a /v2/
prefix, i.e., http://localhost:8080/v2/call instead of
http://localhost:8080/call . In case incompatible changes have to be
introduced to the Model API in the future, the semantics of
invocations of queries with prefix /v2/ will be preserved. This
prefix is only implemented on the Erlang backend; patches are welcome.
|
12.1. Starting the Model API
When an ABS model is started with the -p
parameter naming a port
number, the model will listen on the specified port for requests. In
the following example, we compile a model my-model.abs
and start the
model API on port 8080:
$ absc --java my-model.abs -o my-model.jar $ java -jar my-model.jar -p 8080 # alternatively: $ absc --erlang my-model.abs $ gen/erl/run -p 8080
12.2. Shutting down the Model API
When running a model with the model API activated, it will not return to the command line after the simulation has finished. Instead, the model will keep listening for requests and method calls.
A running model can be terminated manually from the console (for example, via
pressing Ctrl-C
), or by requesting the URL /quit
. The following command
will terminate a model running on port 8080:
$ curl localhost:8080/quit
12.3. Exposing objects
The Model API can be used to access objects of the running model, if these models are exposed.
Objects are exposed via a HTTPName
annotation. In the following example,
two objects of class C
are exposed with the names C1
and C2
respectively. The HTTPName
annotation can be used on assignment statements,
variable declarations and new
expression statements.
[HTTPName: "C1"] new C(); [HTTPName: "C2"] I x = new C();
12.4. Exposing methods
The Model API can be used to call methods of exposed objects, if those methods are exposed in their interface definition. Exposed methods have some restrictions for which arguments they can accept, because their argument values need to be serialized via JSON or URL parameters when called via the Model API.
In an interface declaration, a HTTPCallable
annotation exposes the annotated
method such that it is callable from outside, given an exposed object that implements that interface.
interface I { [HTTPCallable] String method(String param1, Int param2); }
It is a compile-time error if the method takes parameters whose types are not supported.
ABS type | URLencoded format | JSON format |
---|---|---|
|
literal upper- or lowercase |
JSON boolean |
|
a string of digits, e.g., |
JSON integer |
|
a floating-point number, e.g., |
JSON float |
|
URLEncoded text, e.g., |
JSON string |
|
not supported |
JSON list with elements having type |
|
not supported |
JSON map with all values having type |
All others |
not supported |
not supported |
The method can have any return type. Method call results will be returned as
a string via the ABS toString()
function, except for the types enumerated in
the following table.
ABS type | JSON format |
---|---|
|
JSON boolean value |
|
JSON string value |
|
JSON integer |
|
JSON float, via integer division. The behavior is unspecified if
the |
|
JSON float |
|
JSON list, with elements converted one-by-one per this table. |
|
JSON list; guaranteed to contain no duplicate elements, with elements converted one-by-one per this table. |
|
JSON object, with keys generated from their ABS counterpart
via |
Datatype, with at least one named or annotated constructor argument |
JSON object (see below) |
Others |
Converted via ABS |
User-defined datatypes are encoded depending on the presence of accessor
functions and HTTPName
annotations. If the datatype definition contains
neither, values will be encoded as strings via toString()
. If at least one
accessor function or HTTPName
annotation is present, values will be encoded
as objects, with the HTTPName
annotation value (or accessor function name,
if no annotation is present) as key. Unnamed constructor argument values will
not be contained in the JSON object.
ABS definition | Sample JSON encoding |
---|---|
|
|
|
|
|
|
12.5. Querying object state
The Model API can inspect the state of exposed objects, i.e., the names and values of the object’s fields.
The following query returns the names of all exposed objects.
GET http://localhost:8080/o
Inspecting an object state directly can be useful for debugging. The
following query returns a JSON map of the state of the object exposed as C1
,
with object fields as keys.
GET http://localhost:8080/o/C1
The following query returns a JSON map containing the value of C1’s `field
,
with "field"
as key.
GET http://localhost:8080/o/C1/field
When querying for an unknown object or an unknown field, the HTTP request will produce a 404 response code.
12.6. Listing the exposed methods of an object
The following query returns, for an object exposed as C1
, a JSON array of
objects with metadata about callable functions.
GET http://localhost:8080/call/C1
Each entry in the resulting list will be a JSON object with the following keys:
-
name
: the name of the exposed method -
parameters
: an array with one object per parameter, each with the following entries: -
name
: name of the parameter -
type
: type of the parameter -
return
: return type of the method
12.7. Calling exposed methods
Exposed methods are called by querying a URL of the form
http://.../call/<objectname>/<methodname>
Parameters are passed to methods either as query parameters in the URL or in a JSON map passed in as the body of a POST request. For duplicate arguments, parameter values in the URL override values given in the JSON body.
The following query produces the return value of the method call
method("value", 50)
by invoking it on the object exposed as C1
.
GET http://localhost:8080/call/C1/method?param1=value¶m2=50
This query can be invoked from the shell in two ways, using the curl
command, either using query parameters or a JSON body:
$ curl http://localhost:8080/call/C1/method?param1=value\¶m2=50 $ curl -d "{ 'param1': 'value', 'param2': 50 }" http://localhost:8080/call/C1/method
Of course, the Model API can not only be invoked via curl
but also
from other programs. The following example shows how to call a method
testConfig
that takes an argument mylist
of type List<Int>
from
Javascript using the JQuery library:
$.ajax({
url: "call/Model/testConfig",
type: "POST",
data: JSON.stringify({ "mylist": [1,2,3] }),
}).done(function(result) {
console.log("Result: " + JSON.stringify(result));
});
Note the missing solidus (/ ) at the beginning of the query url — this
makes the request robust against custom url prefixes (see
Customizing the Model API URL).
|
Care must be taken to disable timeouts on the HTTP client when querying for long-running methods in this way.
When querying for unknown objects or methods, the HTTP request will produce a 404 response code.
When querying with invalid method parameters, the HTTP request will produce a 400 response code.
When the invoked method throws an exception, the HTTP request will produce a 500 response code.
12.8. The Model API and Timed ABS
The simulated clock of Timed ABS (Timed ABS) is accessible via the Model API.
The current value of the clock can be obtained with the following request:
GET http://localhost:8080/clock/now
The result is a JSON object with a key 'result'
mapping to the
current value of the clock.
If the model was started with a clock limit (see Timed ABS and the Model API), the limit can be increased via a request like the following:
GET http://localhost:8080/clock/advance?by=50
The result is a JSON object with a key 'result'
mapping to the
new clock limit.
This call will always increase the clock limit by the given amount,
even if the clock had not yet reached the previous limit. I.e., when
now()
= 10 and the limit is 20, after the call the limit will be 70,
the same as when the clock was already stopped at the limit of 20 when
the call was received by the Model API.
Note that increasing the clock limit if the model was not started with an initial limit has no effect.
12.9. Customizing the Browser-Based Visualization
Since the Model API is implemented via HTTP, it can be accessed from a
web browser. The --modelapi-index-file
command-line switch is used
to supply an index.html
file at compile-time:
$ absc --java --modelapi-index-file ./index.html *.abs
When running a model on port 8080 and accessing
http://localhost:8080/
from a browser, the contents of that file
will be displayed.
Sometimes it is necessary to add additional files for visualization,
e.g., CSS files, images or JavaScript libraries. The contents of one
directory can be added to the model via the --modelapi-static-dir
command-line switch:
$ absc --java --modelapi-index-file ./index.html --modelapi-static-dir ./support-files/ *.abs
The contents of the given directory are copied at compile-time. The
files within that directory are available within the Model API below
the static/
path. For example, a file ./support-files/js/d3.js
will be accessible as http://localhost:8080/static/js/d3.js
, and can be referred within the index.html
file like this:
<script type="text/javascript" src="static/js/d3.js"></script>
12.10. Customizing the Model API URL
This functionality is only available in the Erlang backend; patches are welcome. |
It is sometimes necessary to change the url under which the model publishes
the given index.html
file and static files. When starting a model, the
parameter --url-prefix
can be used to insert a prefix to these paths, and
to the /call
and /o
URLs described above.
As an example, the following command will make the index file given at compile-time available at http://localhost:8080/abcd/5080/index.html:
$ gen/erl/run -p 8080 --url-prefix /abcd/5080
In the index.html file, do not use an initial solidus (/ ) to
refer to static files and method calls, since such http requests will not work
when the model is run with an url prefix. Instead of <script src="/static/js/d3.js"></script> , write <script src="static/js/d3.js"></script> .
|
13. Timed ABS
Timed ABS is an extension to the core ABS language that introduces a notion of abstract time. Timed ABS can be used to model not only the functional behavior but also the timing-related behavior of real systems running on real hardware. In contrast to real systems, time in an ABS model does not advance by itself. The time model of Timed ABS takes inspiration from formalisms such as Timed Automata and Real-Time Maude.
All untimed ABS models are valid in Timed ABS. An ABS model that contains no time-influencing statements will run without influencing the clock and will finish at time zero. |
The ABS notion of time is dense time with run-to-completion semantics. Timed ABS adds a clock to the language semantics that advances in response to certain language constructs. Time is expressed as a rational number, so the clock can advance in infinitesimally small steps.
It is possible to assign deadlines to method calls. Deadlines are expressed
in terms of a duration value relative to the current time and decrease as the
clock advances. The current deadline of a method is available via the
function deadline
and decreases down to zero.
Time only advances when all processes are blocked or suspended and no process is ready to run. This means that for time to advance, all processes are in one of the following states:
-
the process is awaiting for a guard that is not enabled (see Await (Statement))
-
the process is blocked on a future that is not available (see Get)
-
the process is suspended waiting for time to advance
-
the process is waiting for some resources
In practice this means that all processes run as long as there is “work to be done.”
13.1. Datatypes and Constructors
Time is expressed as a datatype Time
, durations are expressed using the
datatype Duration
, which can be infinite. These datatypes are defined in
the standard library as follows:
data Time = Time(Rat timeValue);
data Duration = Duration(Rat durationValue) | InfDuration;
13.2. Functions
The function now
always returns the current time.
def Time now()
Note that since ABS uses simulated time, two calls to now
can return the same
value. Specifically, the result of now()
changes only by executing
duration
or await duration
statements, or by waiting for resources to
become available.
Time t1 = now();
Int i = pow(2, 50);
Time t2 = now();
assert (t1 == t2); (1)
1 | This assertion will not fail, since no time has passed in the model. Time advance in ABS is explicit and can only occur in suspension points. |
The function deadline
returns the deadline of the current process.
def Duration deadline()
The initial value of a deadline is set via a Deadline
annotation at the
caller site.
Unit m() {
[Deadline: Duration(10)] this!n(); (1)
}
Unit n() {
Duration d1 = deadline(); (2)
await duration(2, 2);
Duration d2 = deadline(); (3)
}
1 | The Deadline annotation assigns a deadline to the process started by the asynchronous method call |
2 | The process can query its current deadline; if no deadline is given, deadline returns InfDuration |
3 | d2 will be two time units less than d1 |
The function timeValue
returns the value of its argument as a rational number.
def Rat timeValue(Time t);
The function timeDifference
returns the absolute value of the difference
between its two arguments, i.e., a value greater or equal to 0
.
def Rat timeDifference(Time t1, Time t2);
The function timeLessThan
returns True
if the value of its first argument
is less than the value of its second argument.
def Bool timeLessThan(Time t1, Time t2);
The function durationValue
returns the value of its argument as a rational number.
def Rat durationValue(Duration t);
The function isDurationInfinite
returns True
if its argument is InfDuration
, False
if its argument is Duration(x)
for some value x
.
def Bool isDurationInfinite(Duration d);
The function addDuration
adds a duration to a time, returning a new value of
type Time
. It is an error to pass InfDuration
as an argument to this
function.
def Time addDuration(Time t, Duration d);
The function subtractDuration
subtracts a duration from a time, returning a
new value of type Time
. It is an error to pass InfDuration
as an argument
to this function.
def Time subtractDuration(Time t, Duration d);
The function durationLessThan
returns True
if the value of its first
argument is less than the value of its second argument. In case both
arguments are InfDuration
, durationLessThan
returns False
.
def Bool durationLessThan(Duration d1, Duration d2);
The function subtractFromDuration
subtracts a value v
from a duration,
returning a new value of type Duration
. In case its first argument is
InfDuration
, the result will also be InfDuration
.
def Duration subtractFromDuration(Duration d, Rat v);
13.3. Statements
The duration(min, max)
statement blocks the cog of the executing process
until at least min
and at most max
time units have passed. The await
duration(min, max)
statement (see Await (Statement)) suspends the current
process until at least min
and at most max
time units have passed.
The second argument to duration
can be omitted; duration(x)
is interpreted
the same as duration(x, x)
.
DurationStmt ::= |
|
AwaitStmt ::= |
|
Guard ::= |
… | DurationGuard |
DurationGuard ::= |
|
The difference between duration
and await duration
is that in the latter
case other processes in the same cog can execute while the awaiting process is
suspended. In the case of the blocking duration
statement, no other process
in the same cog can execute. Note that processes in other cogs are not
influenced by duration
or await duration
, except when they attempt to
synchronize with that process.
A subtle difference between duration and await duration is that in
the latter case, the suspended process becomes eligible for scheduling after
the specified time, but there is no guarantee that it will actually be
scheduled at that point. This means that more time might pass than expressed in the await duration guard!
|
Time t = now(); await duration(1/2, 5); (1) Time t2 = now(); (2)
1 | Here the process suspends for 1/2-5 time units |
2 | t2 will be at least 1/2 time units larger than t |
Time t = now(); duration(1/2, 5); (1) Time t2 = now(); (2)
1 | Here the cog blocks for 1/2-5 time units |
2 | t2 will be between 1/2 and 5 time units larger than t |
13.4. Semantics of Time Advancement
The simulated clock advances such that it makes the least amount of “jumps” without missing any point of interest. This means that when a process waits or blocks for an interval (min, max), the clock will not advance more than max, since otherwise it would miss unblocking the process. On the other hand, the clock will advance by the highest amount allowed by the model. This means that if only one process waits for (min, max), the clock will advance by max.
Figure Big-step time advance shows a timeline with two process, P1
and P2
.
They are waiting for time to advance between (4, 6)
and (3, 5)
units,
respectively. Assuming that no other process is ready to run, the clock will
advance the maximum amount that still hits the earliest interval, in this
case 5. Since the clock is now within both intervals, both processes are
unblocked and ready to run.
13.5. Timed ABS and the Model API
By default, the clock will advance when all processes within the model are waiting for the clock. However, it is sometimes desirable to synchronize the internal clock with some external event or system, and therefore to temporarily block it from advancing beyond a certain value.
When starting a model, the erlang backend (Erlang Backend) supports
a parameter -l x
or --clock-limit x
which stops the clock at the
given value x
. This makes it possible to model infinite systems up
to a certain clock value. When the clock limit is reached, the model
will terminate even if there are processes which would be enabled by
further clock advancement.
When starting a model with both the -l
and -p
parameters (i.e.,
with both the model api and a clock limit), a request to the model api
of the form /clock/advance?by=x
will increase the clock limit by
x
, thereby causing waiting processes to run until the new limit is
reached.
14. User-Defined Schedulers
User-defined schedulers are an experimental feature of Timed ABS. They are available on the Maude and Erlang backends. This section describes the current state of the implementation on the Erlang backend.
All identifiers introduced in this section reside in the ABS.Scheduler
module, which is not imported by default.
See https://doi.org/10.1007/s11334-012-0184-5 for a discussion of ABS user-defined schedulers and process attributes.
14.1. Defining Schedulers
A scheduler is a function that takes a list of processes and chooses one of them. Schedulers are ordinary ABS functions.
The following two example schedulers illustrate the concept. The first, deterministic scheduler takes the first process off the list; the second scheduler chooses a non-deterministic one.
def Process defaultscheduler(List<Process> queue) = head(queue);
def Process randomscheduler(List<Process> queue) = nth(queue, random(length(queue)));
It is possible to formulate an Earliest Deadline First (EDF) scheduler in ABS:
def Process earliest_deadline_scheduler(List<Process> queue) =
foldl((Process a, Process b) =>
if durationLessThan(proc_deadline(a), proc_deadline(b)) // a < b
then a else b)
(queue, head(queue));
All schedulers must have a result type Process
and must take an argument of
type List<Process>
as their first argument.
14.2. Processes and Process Attributes
A process is an ABS abstract datatype; i.e., there is no constructor
available to create a Process
value inside ABS. Processes are created by
the runtime and handed to schedulers.
Processes have attributes which can be used by schedulers to choose the next process to run. For example, a scheduler could always prefer processes that run a certain method. The following attributes are available:
Name | Type | Meaning |
---|---|---|
|
|
Name of the method executed by the process |
|
|
Time when method call was issued |
|
|
The (current) deadline value of the process ( |
|
|
The future of the asynchronous call that is currently being executed by the process |
The attributes cost , start , crit are available in
the Maude backend only but will be implemented in the Erlang backend as well.
The attributes value , finish are under consideration since their
usefulness is questionable.
|
The attribute destinyOf is only available in the Erlang backend.
|
14.3. Using Schedulers
Schedulers apply to cogs since cogs are responsible for scheduling one of
their processes when idle. Since cogs are created via new
expressions, a
scheduler can be given at that point via an annotation. Classes can have a
default scheduler that is given as an annotation to the class definition;
any cog created when instantiating this class will have that scheduler by
default (unless overridden by an annotation at the new
expression).
The following example shows how to define a class that uses the
randomscheduler
by default. The first argument (the list of processes) must
have the name queue
.
[Scheduler: defaultscheduler(queue)] class C implements I {
...
}
The following example shows how to create a cog with a different scheduler.
[Scheduler: randomscheduler(queue)] I instance = new C();
For schedulers with more than one argument, values for subsequent arguments
are filled with the value of object fields of the same name current at
scheduling time. The object whose fields are used is the first object in a
cog, i.e., the object created via the new
expression that created the cog.
Subseqent objects created in that cog via new local
cannot influence the
scheduler.
module Test;
import * from ABS.StdLib.Scheduler;
class C {
Bool flag = False; (1)
}
def Process some_scheduler(List<Process> queue, Bool value) = head(queue); (2)
{
[Scheduler: some_scheduler(queue, flag)] new C(); (3)
}
1 | Here, class C defines a field called flag |
2 | The scheduler some_scheduler has a second argument of type Bool |
3 | The current value of field flag will used as the second argument to some_scheduler |
15. Deployment and Resource Modeling
This chapter describes how to model non-functional properties of systems: code deployment on varying numbers and kinds of machines, and the effects of code locality and different resource types such as CPU speeds, interconnection bandwidth, etc. on the performance of a system.
To simulate the effects of deployment decisions on the cost and performance of running a distributed system, the following aspects need to be added to a model:
-
A notion of location to describe where different parts of the system will run
-
A notion of cost of executing code on such locations; and finally
-
A notion of time that allows us to observe the effects of varying the location and cost.
In ABS, the following language elements implement these aspects:
- Deployment Components
-
ABS cogs can be created on a deployment component (see Deployment Components). Deployment components have a limited number of resources available.
- Resource Annotations
-
ABS statements can be annotated with resource costs (see Resource Types). Executing such annotated statements incurs resource costs.
- Timed ABS
-
The time model of Section Timed ABS naturally extends to observations on resource usage.
These elements work together as in Figure Resource Consumption:
The figure shows a process with a skip
statement with an associated cost of
5, followed by a println
statement with a cost of 10. The deployment
component DC
has 10 resources available in each time interval.
At time t
, the skip
statement consumes the five needed resources and is
successfully executed. The println
statement consumes the remaining five
resources but is not executed yet.
At time t+1
, the deployment component’s resources are refilled. The five
remaining resources of the println
statement are consumed and the statement
is executed. The five remaining resources of DC
are not used.
At time t+2
, the deployment component’s resources are again refilled to 10.
The rest of this chapter explains these language elements concerning resource
modeling in detail. All identifiers introduced in this section reside in the
ABS.DC
module.
15.1. The CloudProvider API
Deployment components are usually managed by a Cloud Provider instance. The
Cloud Provider implements the life cycle shown in Figure
Deployment Component lifecycle. A deployment component is managed by a cloud
provider if it was created via one of the launchInstance
methods.
The operations supported by deployment components during their lifecycle are summarized in Table [table-dc-lifecycle].
State | Starting | Running | Terminating | Terminated |
---|---|---|---|---|
Create objects |
delayed |
yes |
no |
no |
Invoke methods |
delayed |
yes |
no |
no |
Process keep running |
- |
yes |
yes/no |
- |
Using the CloudProvider
To use deployment components via a cloud provider, follow these steps:
-
Create a
CloudProvider
instance -
Set the instance descriptions via the
setInstanceDescriptions
method -
Create deployment components using the
launchInstanceNamed
method -
(optional) Manage access via
releaseInstance
/acquireInstance
-
(optional) Release deployment components via
shutdownInstance
module ProviderDemo;
import * from ABS.DC;
{
CloudProvider p = new CloudProvider("Amazon");
await p!setInstanceDescriptions(
map[Pair("T2_MICRO", map[Pair(Memory,1), Pair(Speed,1)]),
Pair("T2_SMALL", map[Pair(Memory,2), Pair(Speed,1)]),
Pair("T2_MEDIUM", map[Pair(Memory,4), Pair(Speed,2)]),
Pair("M4_LARGE", map[Pair(Memory,8), Pair(Speed,2)])]);
DeploymentComponent dc = await p!launchInstanceNamed("T2_SMALL");
// ... use the deployment component ...
}
Datatypes and Constructors
The type for cloud provider instances is ABS.DC.CloudProvider
.
Cloud provider instances are created with a new CloudProvider(String name)
expression. It is not mandatory but recommended that each cloud provider
instance has a unique name.
It is recommended to call setInstanceDescriptions
once after creating a
cloud provider to set the list of named instance types that this cloud
provider offers.
Methods
This method sets the named instance configurations that the cloud provider
instance should support. These names are used in the methods
launchInstanceNamed
and prelaunchInstanceNamed
.
[Atomic] Unit setInstanceDescriptions(Map<String, Map<Resourcetype, Rat>> instanceDescriptions);
This method returns the map of named instance configurations.
[Atomic] Map<String, Map<Resourcetype, Rat>> getInstanceDescriptions();
This method creates and returns a new deployment component with a resource
configuration corresponding to instancename
, as set by the
setInstanceDescriptions
method. If no description for instancename
exists, launchInstanceNamed
returns null
.
DeploymentComponent launchInstanceNamed(String instancename);
The name of the new deployment component will be "<Cloud provider
name>-<instancename>-<Counter>"
, i.e., a concatenation of the name of the
cloud provider itself, the instance name, and a unique integer as suffix.
If the instance description specifies a startup duration,
launchInstanceNamed
will only return after that amount of simulated time has
elapsed.
The returned deployment component will be acquired (as per acquireInstance
)
and can be used immediately.
This method creates and returns a new deployment component with a resource
configuration corresponding to instancename
, as set by the
setInstanceDescriptions
method. If no description for instancename
exists, prelaunchInstanceNamed
returns null
.
DeploymentComponent prelaunchInstanceNamed(String instancename);
As with launchInstance
, the name of the new deployment component will be
"<Cloud provider name>-<instancename>-<Counter>"
, i.e., a concatenation of
the name of the cloud provider itself, the instance name, and a unique integer
as suffix.
The method prelaunchInstanceNamed
returns immediately, but the method
acquireInstance
, when called on the returned deployment component, will not
return before its startup duration (if specified) has elapsed.
The returned deployment component needs to be acquired (as per
acquireInstance
) before it can be used.
The launchInstance
method creates and returns a new deployment component
with the specified resource configuration. It can be used when, for whatever
reason, the resource configuration should not be registered with the cloud
provider, but the deployment component should still be managed by it.
DeploymentComponent launchInstance(Map<Resourcetype, Rat> description);
The name of the new deployment component will be "<Cloud provider
name>-<Counter>"
, i.e., a concatenation of the name of the
cloud provider itself and a unique integer as suffix.
If the resource configuration specifies a startup duration,
launchInstanceNamed
will only return after that amount of simulated time has
elapsed.
The returned deployment component will be acquired (as per acquireInstance
)
and can be used immediately.
This method creates and returns a new deployment component with the specified
resource configuration. As with launchInstance
, this method can be used
when, for whatever reason, the resource configuration should not be registered
with the cloud provider, but the deployment component should still be managed
by it.
DeploymentComponent prelaunchInstance(Map<Resourcetype, Rat> d)
The name of the new deployment component will be "<Cloud provider
name>-<Counter>"
, i.e., a concatenation of the name of the cloud provider
itself and a unique integer as suffix.
The method prelaunchInstance
returns immediately, but the method
acquireInstance
, when called on the returned deployment component, will not
return before its startup duration (if specified) has elapsed.
The returned deployment component needs to be acquired (as per
acquireInstance
) before it can be used.
This method, together with releaseInstance
, implements exclusive access to a
deployment component. After acquireInstance
returns true, all further
invocations will return false until releaseInstance
is called for the
deployment component.
Bool acquireInstance(DeploymentComponent instance);
If the deployment component passed as argument was not created by the cloud provider, the method returns false.
The methods acquireInstance and releaseInstance are used to
implement exclusive access in a cooperative manner. Attempting to create a
cog on a deployment component without having acquired it beforehand will not
lead to a runtime error; ensuring exclusive access to deployment components is
the responsibility of the modeler.
|
This method releases the deployment component, such that the next call to
acquireInstance
will return true.
Bool releaseInstance(DeploymentComponent instance);
This method returns true if the deployment component was successfully released. It returns false if the deployment component was already not acquired.
If the deployment component passed as argument was not created by the cloud provider, the method returns false.
This method shuts down a deployment component. The effect on the cogs, objects and running tasks deployed on that deployment component are backend-specific.
Bool shutdownInstance(DeploymentComponent instance);
[Atomic] Rat getAccumulatedCost();
[Atomic] Map<String, Map<Resourcetype, Rat>> getInstanceDescriptions();
15.2. Deployment Components
In ABS, processes run inside cogs. Deployment components are used to provide a location to cogs. Cogs residing on the same deployment component share the resources provided by the deployment component.
Deployment Components are first-class constructs in the ABS language.
References to deployment components can be stored in variables of type
DeploymentComponent
, and the methods documented in this section can be
called via asynchronous method calls.
Deployment Components are usually created by a cloud provider instance (see
The CloudProvider API), but can also be created using the new
expression. A
new cog is created on a deployment component by using a DC
annotation to the
new
statement.
If no DC
annotation is given for a new
statement, the fresh cog
(and its first object) are created on the same deployment component as
the current cog.
It is an error to try to create a deployment component via new local .
|
DeploymentComponent dc = await provider!launchInstance(map[Pair(Speed, 10)]); (1) [DC: dc] Worker w = new CWorker(); (2)
1 | The cloud provider provider creates a new deployment component dc with 10 Speed resources |
2 | A new cog containing a CWorker object is created on the new deployment component dc |
All objects of a cog must reside on the same deployment component, i.e.,
[DC: x] new local C() is an error.
|
Resources and Deployment Components
As seen above, each deployment component “carries” some amount of resources
for each resource type. This is expressed as a map from resource type to a
number, for example map[Pair(Speed, 10), Pair(Bandwidth, 20)]
. When no
amount is given for some resource type, it is infinite. See Resource Types
for a description of the available resource types.
Methods
Return the load (0-100) for the given resource type rtype
over the last n
periods. If the deployment component was created with infinite resources for
the given resource type, load
returns 0
.
Return the total available amount for the given resourcetype. If the
deployment component was created with infinite resources for the given
resource type, total
returns InfRat
, otherwise Fin(value)
.
Decrease the total available amount for the given resourcetype by
amount
from the next time interval onwards. Trying to decrement
infinite resources has no effect. Trying to decrement by more than
available will decrement the maximum amount possible. Returns the
amount by which the resource was decreased.
Increase the total available amount for the given resourcetype by amount
from
the next time interval onwards. Trying to increment infinite resources has no
effect. Returns the amount by which the resource was increased.
Transfer amount
resources of type rtype
from the current
deployment component to target
. Takes effect on the next time
period. Returns the amount transferred, which will be lower than
amount
in case the deployment component has less resources
available.
(This method is implemented via decrementResources
and
incrementResources
.)
Returns the name of the deployment component. Deployment components created
via a CloudProvider
are guaranteed to have a unique name if no two cloud
providers have the same name.
Get the creation time of the deployment component, in terms of the simulated clock.
Get the specified startup duration, or 0 if none specified.
Get the specified shutdown duration, or 0 if none specified.
Get the specified payment interval, or 1 if none specified.
Get the specified cost (price) per interval, or 0 if none specified.
Shut down the deployment component. It is an error to create a new object on a deployment component that has been shutdown, or to invoke a method on an object residing on a deployment component that has been shut down.
Get the cloud provider that manages this deployment component. Returns null
if the deployment component was not created by a cloud provider. See
The CloudProvider API for a discussion of cloud providers.
Convenience method for calling acquireInstance
of the associated cloud
provider. If no cloud provider is set, returns True
. See
The CloudProvider API for a discussion of cloud providers.
Convenience method for calling releaseInstance
of the associated cloud
provider. If no cloud provider is set, returns True
. See
The CloudProvider API for a discussion of cloud providers.
15.3. Resource Types
The term “Resource” can be understood in different ways. In ABS, we define “Resource” to be a countable, measurable property of a deployment component. Some resources stay constant throughout the life of a deployment component (e.g., the number of cores), some others are influenced by program execution (e.g., the available bandwidth in the current time slot).
Resource usage is not allowed in init blocks. |
The resource types currently supported by the ABS language are defined in the
ABS.DC
module as follows:
data Resourcetype = Speed | Bandwidth | Memory | Cores ;
When a deployment component is created without explicitly giving a value for a resource type, it is assumed to have an infinite amount of that resource. E.g., when modeling a denial of service attack, the deployment component running the attacker code will have infinite speed and bandwidth.
Speed
The Speed
resource type models execution speed. Intuitively, a deployment
component with twice the number of Speed
resources will execute twice as
fast. Speed resources are consumed when execution in the current process
reaches a statement that is annotated with a Cost
annotation.
Time t1 = now(); [Cost: 5] skip; Time t2 = now();
Executing the above skip
statement will consume 5 Speed resources from the
deployment component where the cog was deployed. If the deployment component
does not have infinite Speed resources, executing the skip
statement might
take an observable amount of time, i.e., t1
and t2
might be different.
Bandwidth
Bandwidth is a measure of transmission speed. Bandwidth resources are
consumed during method invocation and return
statements. No bandwidth is
consumed if sender and receiver reside on the same deployment component.
Bandwidth consumption is expressed via a DataSize
annotation:
Time t1 = now(); [DataSize: 2 * length(datalist)] o!process(datalist); Time t2 = now();
Executing the above method invocation statement will consume bandwidth
resources proportional to the length of list datalist
.
Memory
The Memory
resource type abstracts from the size of main memory, as a measure
of how many and which cogs can be created on a deployment component. In
contrast to bandwidth and speed, memory does not influence the timed behavior
of the simulation of an ABS model; it is used for static deployment modeling.
Cores
The Cores
resource type expresses the number of CPU cores on a deployment
component. It is used for static deployment decisions and does not have
influence on the timing behavior of simulations (use the Speed
resource type
for this purpose).
15.4. Modeling Resource Usage
As described above, resource information is added to statements of an ABS
model using Cost
and DataSize
annotations. Executing such annotated
statements causes observable changes in the simulated time and deployment
component load during simulation.
module Test; import * from ABS.DC; (1) interface I { Unit process(); } class C implements I { Unit process() { [Cost: 10] skip; (2) } { DeploymentComponent dc = new DeploymentComponent("Server", map[Pair(Speed, 5), Pair(Bandwidth, 10)]); [DC: dc] I i = new C(); [DataSize: 5] i!process(); (3) }
1 | Make all necessary identifiers accessible in the current module |
2 | Executing this statement costs 10 Speed units; the time needed depends
on the capacity of the deployment component, and on other cogs executing in
parallel on the same deployment component. |
3 | Executing this method call consumes 5 Bandwidth resources. Since dc has
10 bandwidth resources per time unit, the message will be transported
instantly. Executing the skip statement in the method body will not finish
instantaneously because dc only has 5 Speed resources in total. |
16. Software Product Line Engineering
ABS supports the development of software product lines with a set of language constructs for defining system variants. Following a feature-oriented software development approach, variants are described as sets of features. Features and their dependencies are specified in a feature model. Each feature has its corresponding ABS implementation. A feature’s implementation is specified in terms of the code modifications (i.e., additions and removals) that need to be performed to a variant of the system that does not include that feature in order to add it. This style of programming is called delta-oriented programming. The ABS code modules that encapsulate feature implementations are called delta modules (deltas).
16.1. Delta-Oriented Programming
ABS supports the delta-oriented programming model, an approach that aids the development of a set of programs simultaneously from a single code base, following the software product line engineering approach. In delta-oriented programming, features defined by a feature model are associated with code modules that describe modifications to a core program. In ABS, these modules are called delta modules. Hence the implementation of a software product line in ABS is divided into a core and a set of delta modules.
The core consists of a set of ABS modules that implement a complete software product of the corresponding software product line. Delta modules (or deltas in short) describe how to change the core program to obtain new products. This includes adding new classes and interfaces, modifying existing ones, or even removing some classes from the core. Delta modules can also modify the functional entities of an ABS program, that is, they can add and modify data types and type synonyms, and add functions.
Deltas are applied to the core program by the ABS compiler front end. The choice of which delta modules to apply depends on the selection of a set of features, that is, a particular product of the SPL. The role of the ABS compiler front end is to translate textual ABS models into an internal representation and check the models for syntax and semantic errors. The role of the back ends is to generate code for the models targeting some suitable execution or simulation environment.
DeltaDecl ::= |
|
DeltaParam ::= |
Type SimpleIdentifier |
HasCondition ::= |
|
ModuleAccess ::= |
|
ModuleModifier ::= |
|
ClassModifier ::= |
|
InterfaceModifier ::= |
|
The DeltaDecl
clause specifies the syntax of delta modules, consisting of a
unique identifier, an optional list of delta parameters, an optional module
access directive, and a sequence of module modifiers.
The ModuleAccess
clause specifies the semantics of unqualified identifiers
within a delta. Any identifiers without namespace prefix will be resolved as
if they occurred in the module named in the ModuleAccess
clause. A delta
can always make additions and modifications in any module by fully qualifying
the TypeName
within module modifiers. New program elements (classes,
interfaces, etc.) with unqualified names are added to the module given in the
uses
clause; it is an error to add new program elements with unqualified
names in a delta that does not have a uses
clause.
While delta modeling supports a broad range of ways to modify an ABS model, not all ABS program entities are modifiable. These unsupported modifications are listed here for completeness. While these modifications could be easily specified and implemented, we opted not to overload the language with features that have not been regarded as necessary in practice:
- Class parameters and init block
-
Deltas currently do not support the modification of class parameter lists or class init blocks.
- Deltas
-
currently only support adding functions, and adding and modifying data types and type synonyms. Removal is not supported.
- Modules
-
Deltas currently do not support adding new modules or removing modules.
- Imports and Exports
-
While deltas do support the addition of import and export statements to modules, they do not support their modification or removal.
- Main block
-
Deltas currently do not support the modification of the program’s main block.
16.2. The Feature Model
Software variability is commonly expressed using features which can be present or absent from a product of the product line. Features are defined and organised in a feature model, which is essentially a set of logical constraints expressing the dependencies between features. Thus the feature model defines a set of legal feature combinations, which represent the set of valid software variants that can be built.
Specifying the Feature Model
The FeatureModel
clause specifies a number of "orthogonal" root feature
models along with a number of extensions that specify additional constraints,
typically cross-tree dependencies. Its grammar is as follows:
FeatureModel ::= |
{ |
FeatureDecl ::= |
FName [ |
FeatureExtension ::= |
|
Group ::= |
|
Cardinality ::= |
|
AttributeDecl ::= |
|
Limit ::= |
IntLiteral | |
Constraint ::= |
Expr |
Expr ::= |
|
UnOp ::= |
|
BinOp ::= |
|
Attributes and values range over integers, strings or booleans.
The FeatureDecl
clause specifies the details of a given feature, firstly by
giving it a name (FName
), followed by a number of possibly optional
sub-features, the feature’s attributes and any relevant constraints.
The FeatureExtension
clause specifies additional constraints and attributes
for a feature, and if the extended feature has no children a group can also be
specified. This is particularly useful for specifying constraints that do not
fit into the tree structure given by the root feature model.
Here is an example feature model for the DeltaResourceExample
product line,
defining valid combinations of features and valid ranges of parameters for
cost, capacity and number of machines:
root Calculations {
group oneof {
Wordcount,
Wordsearch
}
}
root Resources {
group oneof {
NoCost,
Cost { Int cost in [ 0 .. 10000 ] ; }
}
}
root Deployments {
group oneof {
NoDeploymentScenario,
UnlimitedMachines { Int capacity in [ 0 .. 10000 ] ; },
LimitedMachines { Int capacity in [ 0 .. 10000 ] ;
Int machinelimit in [ 0 .. 100 ] ; }
}
}
Feature Model Reflection
There is support for limited reflection on the feature model and configured
product in the module ABS.Productline
. The datatype Feature
contains
constructors for all feature names. The function product_features
returns a
list of features contained in the current product, and product_name
returns
the name of the product, or the empty string if no product was specified.
The following sample code shows the usage, assuming that product Product
was
generated:
module Test;
import * from ABS.Productline;
{
List<Feature> foo = product_features(); // => Cons(FeatureA, Cons(FeatureC, Nil))
String name = product_name(); // => "Product"
}
productline Test;
features FeatureA, FeatureB, FeatureC;
product Product(FeatureA, FeatureC);
16.3. Software Product Lines and Products
A (software) product line is a set of software variants that can be built by selecting any combination of features allowed by the feature model and applying the deltas that provide the implementation for those features to the core program. How features are associated with their implementation is defined in ABS with a SPL configuration.
An ABS product is simply a set of features associated with a name.
Specifying the Product Line
The ABS configuration language links feature models, which describe the structure of a SPL, to delta modules, which implement behavior. The configuration defines, for each selection of features satisfied by the product selection, which delta modules should be applied to the core. Furthermore, it guides the code generation by ordering the application of the delta modules.
Configuration ::= |
|
Features ::= |
|
DeltaClause ::= |
|
DeltaSpec ::= |
DeltaName [ |
DeltaName ::= |
TypeId |
DeltaParams ::= |
DeltaParam { |
DeltaParam ::= |
FName | FName |
AfterClause ::= |
|
WhenClause ::= |
|
AppCond ::= |
AppCond |
Features and delta modules are associated through application conditions (a.k.a. activation conditions), which are logical expressions over the set of features and attributes in a feature model. The collection of applicable delta modules is given by the application conditions that are true for a particular feature and attribute selection. By not associating the delta modules directly with features, a degree of flexibility is obtained.
Each delta clause has a DeltaSpec
, specifying the name of a delta module
name and, optionally, a list of parameters; an AfterClause
, specifying the
delta modules that the current delta must be applied after; and an application
condition AppCond
, specifying an arbitrary predicate over the feature names
(FName
) and attribute names (AName
) in the feature model that describes
when the given delta module is applied.
productline DeltaResourceExample;
features Cost, NoCost, NoDeploymentScenario, UnlimitedMachines, LimitedMachines, Wordcount, Wordsearch;
delta DOccurrences when Wordsearch;
delta DFixedCost(Cost.cost) when Cost;
delta DUnboundedDeployment(UnlimitedMachines.capacity) when UnlimitedMachines;
delta DBoundedDeployment(LimitedMachines.capacity, LimitedMachines.machinelimit) when LimitedMachines;
Specifying Products
ABS allows the developer to name products that are of particular interest, in order to easily refer to them later when the actual code needs to be generated. A product definition states which features are to be included in the product and sets attributes of those features to concrete values. In the simplest case products are declared directly, by listing the features that they include. It is also possible to declare products based on other products using product expressions. Product expressions use set-theoretic operations (union, intersection, complement) over products and sets of features.
Selection ::= |
|
ProductExpr ::= |
|
FeatureSpecs ::= |
FeatureSpec { |
FeatureSpec ::= |
FName [ AttributeAssignments ] |
AttributeAssignments ::= |
|
AttributeAssignment ::= |
AName |
Here are some product definitions for the DeltaResourceExample
product line:
product WordcountModel (Wordcount, NoCost, NoDeploymentScenario);
product WordcountFull (Wordcount, Cost{cost=10}, UnlimitedMachines{capacity=20});
product WordsearchFull (Wordsearch, Cost{cost=10}, UnlimitedMachines{capacity=20});
product WordsearchDemo (Wordsearch, Cost{cost=10}, LimitedMachines{capacity=20, machinelimit=2});
Here are some product definitions for the CharityOrganizationExample
with ProductExpr
:
product Org1 = SekolahBermainMatahari || {Continuous};
product Org2 = SekolahBermainMatahari || {Continuous, Automatic_Report};
product Org3 = SekolahBermainMatahari || PKPU;
product Org4 = SekolahBermainMatahari || PKPU || RamadhanForKids;
product Org5 = SekolahBermainMatahari || PKPU || RamadhanForKids || BeriBuku;
product Org6 = SekolahBermainMatahari && RamadhanForKids;
product Org7 = SekolahBermainMatahari && BeriBuku;
product Org8 = SekolahBermainMatahari - {Eventual};
product Org9 = SekolahBermainMatahari - {Eventual, Income};
product Org10 = SekolahBermainMatahari && RamadhanForKids || {Money, Item};
product Org11 = SekolahBermainMatahari && (RamadhanForKids || {Money, Item});
Checking the SPL
Because the number of variants in an SPL can be very large, checking them efficiently (e.g., to ensure that they are all well-typed) is challenging. Building each variant in order to type-check it is usually not feasible from a performance perspective. Instead, the ABS compiler employs a number of efficient consistency checks. These fall into two categories.
-
Family-based analysis steps operate on the SPL definition itself,
-
Analysis steps operate on lightweight abstractions of the SPL variants.
These checks are performed automatically upon compilation and help ensure that all variants defined by an SPL specified in ABS can be built and are well-typed ABS programs.
17. ABS Backends
This section describes the available and supported backends for ABS. Different backends have different purposes (simulation, code execution, visualization). Their respective section describes their features and usage.
The following table gives an overview of the features that different backends have.
- Real-Time ABS
-
Simulating a dense-time clock, and language constructs expressing delays and task deadlines. Used for simulating time usage of ABS models.
- Resource Models
-
Specification of resource availability (processor power, bandwidth) of Deployment Components and simulation of resource usage deployed thereon. Builds on the semantics of Real-Time ABS.
- Deployment
-
Running ABS code on a number of physical or virtual machines, with support for creating new cogs remotely.
- FLI
-
Foreign-Language Interface, the ability to call backend-specific native code from ABS.
Backend | Maude | Erlang | Haskell | Java |
---|---|---|---|---|
Real-Time ABS |
yes |
yes |
- |
yes |
User-defined Schedulers |
yes |
partial |
- |
- |
Resource Models |
yes |
yes |
- |
yes |
Deployment |
- |
- |
yes |
yes |
FLI |
- |
- |
yes |
planned |
Model API |
- |
yes |
- |
yes |
17.1. Maude Backend
The Maude backend is a high-level, executable semantics in rewriting logic of the ABS language. Due to its relatively compact nature, it serves as a test-bed for new language features.
Executing a model on the Maude backend results in a complete snapshot of the system state after execution has finished.
The main drawback of the Maude backend is its relatively poor performance, making it not very suitable to simulate large models.
Features:
-
CPU and bandwidth resources
-
Simulation of resource usage on deployment components
-
Timed semantics
-
Executable formal semantics of the ABS language
How to run the Maude backend
Running a model on Maude involves compiling the code, then starting Maude with the resulting file as input.
Compiling all files in the current directory into Maude is done with the following command:
$ absc --maude *.abs -o model.maude
The model is started with the following commands:
$ maude Maude> in model.maude Maude> frew start .
This sequence of commands starts Maude, then loads the compiled model and starts it. The resulting output is a dump of the complete system state after execution of the model finishes.
In case of problems, check the following:
-
absc
should be in the path; check thePATH
environment variable. -
absfrontend.jar
should be in the environment variableCLASSPATH
.
17.2. Erlang Backend
The Erlang backend runs ABS models on the Erlang virtual machine by translating them into Erlang and combining them with a small runtime library implementing key ABS concepts (cogs, futures, objects, method invocations) in Erlang.
Executing an ABS model in Erlang currently returns the value of the last
statement of the main block; output via ABS.StdLib.println
is printed on the
console. For additional introspective and interactive capabilities, the
Erlang backend supports a Model API (see below).
How to run the Erlang backend
Running a model in Erlang involves compiling the ABS code, then compiling and running the resulting Erlang code.
Compiling all files in the current directory into Erlang and starting the resulting model is done with the following commands:
$ absc --erlang *.abs $ gen/erl/run
This sequence of commands starts Erlang, then compiles the generated Erlang
code and starts it. Type gen/erl/run -h
for a list of options accepted by
the model.
Recording and replaying traces
ABS task scheduling is non-deterministic; i.e., when two tasks are enabled, the cog will select an arbitrary one (but see User-Defined Schedulers). The erlang backend can record a trace of scheduling decisions and replay it to precisely reproduce the previous run.
To record a trace to a file trace.json
, start the model with a parameter
--record trace.json
or -t trace.json
.
To replay an existing trace recorded in trace.json
, start the model with
--replay trace.json
or -r trace.json
.
A trace can also be obtained from a running model via the Model API. Assuming
the model is started on port 8080 (via a parameter -p 8080
), the trace is
accessible at the url http://localhost:8080/trace. A trace visualizer can be
found here: https://github.com/larstvei/ABS-traces.
Generating code coverage information
The Erlang backend can optionally generate code coverage information in a
format inspired by gnu gcov
(see
https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html). The coverage
information contains line numbers and execution count, but not the source code
itself. This is sufficient for some tools to visualize code coverage, e.g.,
cov-mode
for Emacs (https://github.com/AdamNiederer/cov).
To generate code coverage information, compile an abs model with the
--debuginfo
switch, then run it as normal, i.e.,
$ absc --erlang --debuginfo *.abs $ gen/erl/run
For each .abs
file, running the model will generate a .abs.gcov
file in
the directory gen/erl/absmodel
after the simulation finishes.
17.3. Java Backend
The Java backend runs ABS models on the JVM by translating them into Java and combining them with a runtime library implementing key ABS concepts.
How to run the Java backend
Compiling all files in the current directory into Java and starting the resulting model is done with the following commands:
$ absc --java *.abs -o model.jar $ java -jar model.jar
The first command compiles the ABS model, the second command starts Java and
runs the code contained in model.jar
. In case more than one abs file
contains a main block, an arbitrary main block is executed.
The source code of the generated classes can be inspected below the gen/
directory.
17.4. Haskell Backend
The Haskell backend translates ABS models to Haskell source code,
consequently compiled through a Haskell compiler and executed by the machine.
The backend, albeit a work in progress, already supports most ABS constructs
and, above that, augments the language with extra features, such as Type Inference
,
Foreign Imports
and real Deployment Components
.
With the feature of Type Inference
enabled, the user can optionally leave out
the declaration of types of certain expressions; the backend will be responsible
to infer those types and type-check them in the ABS program. The type inference
is safe, in the sense that it will not infer any wrong types (soundness property).
To make use of this feature, the user puts an underscore _
in place
of where a type would normally be, as in this ABS block of code:
{ _ x = 3;
Int y = 4; // type inference is optional
x = x+y;
_ l = Cons(x, Cons(y, Nil));
_ s = length(l) + 4; }
At the moment, the type inference cannot infer
interface types as in _ o = new Class(); .
It can however infer all the other types, that is Builtin, Algebraic, and Exception data types.
There is a plan to support this in the future.
|
The Haskell backend extends the ABS module system with the ability
to include Haskell-written code inside the ABS program itself.
This feature is provided by the foreign_import
keyword,
which syntactically follows that of the normal import
keyword. To illustrate this:
module Bar;
...
foreign_import Vertex from Data.Graph;
foreign_import vertices from Data.Graph;
the programmer has imported the Vertex
algebraic datatype and
the vertices
function from the Data.Graph
Haskell library module into an ABS module
named Bar
. Any imported Haskell term will be treated as its ABS
counterpart. In the example case, the programmer may re-export the foreign terms
or use them as normal ABS terms:
{
Graph g = empty_graph();
List<Vertex> vs = vertices(g);
}
At the moment, the ABS programmer can reuse (with foreign_import ) Haskell’s Algebraic Data types
and Pure functions, but not monadic IO code (Haskell code with side-effects). This restriction
is planned to be lifted in a later release of the backend.
|
The Haskell backend implements the ABS feature of Deployment Components, faithfully as described in Chapter 8. The backend follows the view that Deployment Components are virtual machines running in the Cloud. As such, each single DC corresponds to one Cloud virtual machine (VM).
Two DC classes (implementations) are provided to support the OpenNebula and Microsoft Azure cloud computing platforms accordingly:
class NebulaDC(CPU cpu, Mem memory) implements DC {
...
}
class AzureDC(CPU cpu, Mem memory) implements DC {
...
}
The CPU
and Mem
datatypes are passed as arguments when creating the DC to parameterize
its computing resources. These datatypes are simple defined as type synonyms to
Int, but you can expect more sophisticated resource encodings for a future backend release.
type CPU = Int; // processor cores
type Mem = Int; // RAM measured in MB
The backend has only been developed on and tested against the OpenNebula platform. This hopefully will change when more cloud providers will be supported. |
How to obtain and install
The compiler itself is written in Haskell and distributed as a normal Haskell package. Therefore to build abs2haskell you need either
1) a recent release of the Haskell platform (version >= 2013.2.0.0),
2) the GHC compiler accompanied by the Cabal packaging system:
-
GHC compiler (version >=7.6)
-
Cabal package (version >=1.4)
-
cabal-install
program. The compiler depends on other community packages/libraries. This program will automatically fetch and install any library dependencies.
Clone the repository with the command:
$ git clone git://github.com/bezirg/abs2haskell
To build and install the abs2haskell backend run inside the abs2haskell/
directory:
sudo make install
How to run the Haskell backend
After installing the compiler, you should
have the program abs2haskell
under your PATH
.
Examples of running:
$ abs2haskell Example.abs
# An ABS program may have multiple main blocks in different modules.
# So you have to specify in which module is the main block you want to build with
$ abs2haskell --main-is=Example.abs Example.abs
$ abs2haskell examples/ # will compile all ABS files under examples directory
The compiler will generate ".hs" files for each compiled ABS module. No other runtime system libraries and dependencies will be generated.
The final step before running the ABS program is to compile the generated Haskell code to machine code, as the example:
ghc --make -threaded Example.hs # put the generated haskell file that has the main block here
./Example -O # means run it on 1 core with default optimizations
./Example -O +RTS -N1 # the same as the above
./Example -O +RTS -N2 # run it on 2 cores
./Example -O +RTS -N4 # run it on 4 cores
./Example -O +RTS -NK # run it on K cores