ContRap
The symbolic tool for software rapid prototyping
Documentation
User level

The user level mode of ContRap provides a comfortable and comprehensive interface for manipulating C/C++ functions and classes. The language is designed to minimize the code length while preseving a comprehensive algorithm presentation.

Symbolic and strict modes

The default user level behavior of ContRap is the symbolic mode. This means that each unknown identifier evaluates to itself. This behavior is motivated by the idea to write programs with semantics defined as late as possible, at the point of usage.

To switch between the symbolic and the strict modes the global Boolean variable ''strict'' is used. When it is set to true, the interpreter switches to the strict mode.

strict := false // Switching to the symbolic mode
a := b(2) + 1 // At this point b is undefined
b := function(x) x*x
a // Returns 5

As opposed to the symbolic mode, ContRap provides the so-called strict mode. In strict mode ContRap reports an error when the interpreter encounters an undefined identifier.

strict := true // Switching to the strict mode
a := b(2) + 1 // Reports an error: Unknown identifier 'b'

Built-in types

The philosophy of ContRap is to exclusively use types, functions, and classes exported from C/C++ plugin libraries. There are only a few built-in types, which are exclusively exported by the core library of ContRap: Boolean values, strings, lists, and sequences.

Boolean: Boolean values take one of the constant values ''true'' or ''false'' and are used as a base type for comparisons in conditional statements. The Boolean constants are recognized natively by the ContRap parser. On the C/C++ level the Boolean values correspond to the type ''bool''.

Strings: Strings are essential for user interaction and are, therefore, provided as a basic data type in ContRap. Strings are natively processed by the ContRap parser, when it encounters an expression enclosed in double quotes:

"String value"

On the C/C++ level a string corresponds to the class ''String'', which is a derivation of the ''std::string'' struct.

Lists: The List class is the only native container type of ContRap. Lists are comma-separated object collections enclosed in square brackets:

[1,2,a,"b",[1,2]]

Lists are type-polymorphic. In other words, as shown by the above example, a list may contain any collection of ContRap objects regardless of their type. Lists are processed natively by the ContRap parser.

Number constants interpreted by the parser are implicitly converted at parse time. This is done by calling the functions ''Integer'' or ''Floating'' with the string token of the number. You can redefine these two functions to parse your own numbers.

Integers: Integers are interpreted by the parser by calling the function ''Integer''. An integer starts with a number, a ''-''-symbol or with 0x and does not contain any other symbols as numbers or hexadecimal numbers respectively:

01234567890 // Decimal integer value
0x0AFFBCE // Hexadecimal integer value

Floating point numbers: Finite precision floating numbers are interpreted by the function ''Floating''. Floating point numbers start with a ''-''-symbol, a dot, or a number and are not integers:

0.10020 // Regular floating value
10e+10 // Floating value with a power
.10e-10 // Floating value with a trailing dot

Control flow structures

Control flow is realized by statements, sequences, blocks, conditional statements, and function calls. Read more about functions in the functions section.

A statement is an ordered group of commands written one after the other.

a := 1 1 x = y "hello"

From the syntactical rules of the language it is clear where the one command stops and where the next command starts. A statement is evaluated from the left to the right within the current scope. The result of a statement is the result of the last command processed.

A sequence is a ordered group of expressions separated by commas.

1, a, x = y, "hello"

A sequence is evaluated from the left to the right within the current scope. The result of a sequence evaluation is the sequence of evaluated components.

A block is a statement enclosed by the ''begin'' and ''end'' keywords. In contrast to a sequence, a block has its own scope. In other words, locally declared variables are only visible within the scope and are unassigned after the control flow reaches the end of the scope.

begin a b c d end

The return value of a block statement is the return value of the statement inside the block.

There are two sorts of conditional commands: conditional branches and loops. Branches are realized by the ''if''-construct:

if condition then true-case else else-case

If and only if the condition evaluates to the Boolean constant ''true'' the true-case command is executed. Otherwise the else-case command is executed. The else-branch is optional.

There are two types of loops: A ''for''-loop and a ''while''-loop. The ''for''-loop has several different forms. The syntax is as follows:

for init to value step increment do body
for init till condition step increment do body
while condition do body

In the first case there is an implicit ''condition'' statement, which is ''variable <= value'' derived from the init statement and the ''value''. The ''step''-statements are optional.

All loops evaluate in each run the ''condition''-expression and execute the ''body''-command, if the condition has evaluated to the Boolean constant ''true''. Within the ''for''-loop the ''init''-statement is executed before the first run. After each ''body''-execution the ''increment''-statement is called.

Both ''while''- and ''for''-loops can be terminated by calling the ''break''-command.

Variables and binding

Variables are symbolic names for expressions distinct to the set of the keywords. Variables do not have a type (but the object they point to). In other words, ContRap implements weak dynamic typing in the user-level mode. Hence, any ContRap object can be assigned to any variable using the '':=''-operator.

a := 1 // Assign the value 1 to the variable a

ContRap reacts differently when assigning function objects to variables.

A variable belongs to a scope. Scopes are tree-like visibility areas introduced by control flow structures. A variable is visible to all sub-scopes of the variable scope but not to the parent scopes. There is a global parent scope to all ContRap scopes - the user level scope.

When ContRap evaluates a variable, it first searches in the local scope and then recursively in the parent scope of the local scope. If the name is not known in the end, the result is, according to the symbolic evaluation principle, the variable itself. To fix the scope of a variable to the local scope it has to be declared ''local'':

a := 1
begin local a := 2 end // Declaration can be mixed with an assignment
a // Here the variable has the value 1

Functions

Functions encapsulate frequently used code. A function is a list of atomic functions, which share a similar functionality. ContRap's user level does generally not make any difference between functions defined in the ContRap language or exported from C/C++.

An atomic function is defined by the ''function''-keyword:

function(parameter1:Type1=Default1,...,parametern:Typen=Defaultn; option1:Type1=Default1,... ,optionm:Typem=Defaultm):Type where condition body

The ''parameteri''-identifiers define the symbolic names of the inputs of the function, which are used within the function scope. Enumerated ''Typei''-s impose type requirements on the inputs and the ''Type''-identifier defines the output type. All the type expressions and the default values for the parameters are optional. The function options are separated by a semicolon. The default values are required. The keyword ''where'' precedes the optional execution condition.

A call to a function is syntactically written as follows:

name(argument1,...,argumentn)

Here, the object ''name'' is an expression which evaluates to a function or to an atomic function. The function is evaluated as follows:

  • The arguments are evaluated in the caller-scope.
  • An atomic function from the list is selected, which fits best the arguments. This includes the implicit conversion and evaluation of the condition, if available.
  • The function body is executed.
  • The return value of the function body is implicitly converted to the output type.
Read more about the call mechanism in the next section.

Since a function or an atomic function is a regular ContRap object, it can be assigned to variables within any scope. This includes functions-in-functions, as shown by the following example:

f := function(x) x^2
g := function(x) begin
  local h := function(y) y-1
  f(h(x))
end

ContRap functions are polymorphic. To implement that there is an essential difference when assigning atomic functions to a variable associated with a function. The new atomic function is joined to the list of functions already associated with the name, if the signature of the new function is different. If not, the old function with the same signature is replaced in the list.

f := function(x:Integer) x^2
f := function(x:Floating) x+x
f(3) // Returns 9
f(3.0) // Returns 6

As a result of the above assignments the function ''f'' behaves differently when called with integers or with floating point numbers.

Function options

The type of the functions arguments determines if a function is suitable to be called with the given values. This is not always appropriate, as the following example demonstrates. Consider the function ''f'' defined piecewise as

f := function(x:int,y:int) x+y
f := function(x:int,y:double) x*y

If you call f(1,''1'') you will get 1 as an answer. This is due to the fact that the function with the double argument was defined as last. To outcome this virtually arbitrary behaviour you can declare ''y'' to be a parameter, which does not affect the function selection process. In ContRap, such parameters are called ''options''. You separate options from other arguments by a semicolon:

f := function(x:int,y:int) x+y
f := function(x:int;y:double=1.0) x*y

Now a call to f(1,''1'') yields 2 because the only function with the name ''f'', which takes two arguments is the first definition of ''f''. The second one takes only one argument but an option with the name ''y''. You call the second version of ''f'' by

f(1;y=''1'')

Which argument is a regular parameter or an option is a philosophical software design question. In ContRap options are frequently used as ''adjustment'' parameters of algorithms.

Conditional calls

When an atomic function requires certain signature for its input parameters, but the user specifies a different one, the function is ignored, as if it would not be defined. If there is no atomic function suitable for calling with given parameters, the result evaluates to a function call with evaluated parameters:

f := function(x:Integer) x+x
f("a"+"b") // Results in f("ab")

The above procedure can be seen as a condition on the input variable ''x'' of the sort type(x) = ''Integer''. ContRap extends this concept to arbitrary calling conditions. These are specified in the ''condition''-statement of the function definition. If the condition is not satisfied the interpreter behaves as if the atomic function was not defined.

// Computes A^t A for a symmetric matrix
f := function(A:matrix) where A^t = A
   A*A

The same result can be achieved with more computational cost by calling the ''abort'' statement followed by an error message. A function which calls to abort causes the interpreter to continue the search for a suitable function.

// Computes A^t A for a symmetric matrix
f := function(A:matrix)
  if A^t = A then
    A*A
  else
     abort("Non symmetric matrix")

Read more about the error handling in the error section.

Implicit conversion

ContRap is designed as an interface between several libraries with different data types expressing the same semantical objects. For example, by using a numerical (e.g. LAPACK), a symbolical (e.g. GiNaC), and a self-made libraries for matrix computation you end up having three different data types for the object ''matrix''. It is a central feature to make the conversion between those different representations as transparent as possible. Implicit conversion is a tool to realize this goal.

An implicit convertor from the Type ''B'' to type ''A'' is a function with a special signature assigned to the type name ''A''. It has one argument of type ''B'' and returns objects of type ''A''. It may be thought as a copy-constructor of a C/C++ class joined to the implementation of the object ''A''.

function(x:B):A

Implicit convertors are applied to the arguments of a function whenever the type of an argument does not match the expected argument type of a function. For example, if a function requires the type ''A'' but receives the type ''B'' ContRap checks at run-time, whether an implicit convertor to the type ''A'' exists. In the positive case the argument of type ''B'' is automatically passed to this function before calling the original one.

This way you can combine objects of different types in expressions.

A := matrix([[1,2],[3,4]]) - lambda*I(3) // GiNaC matrix
v := lvector_t([1,2,3]) // LAPACK vector
(C^t*C)^(-1)*v // The arguments are implicitly converted to a common type

In the above example there is no ambiguity of which implicit convertor to use: The matrix ''A'' contains symbolic elements, which can not be converted to a LAPACK object. If such an ambiguity exists, then the order of the operator definition decides which operator is used. According to the calling sequence of ContRap functions the latest defined function is called when ambiguities exist. For example, if you load the GiNaC and then the LAPACK package, ContRap will try to convert the objects to LAPACK matrices first. In such a case you can explicitly convert the arguments to avoid confusion.

A := matrix([[1,2],[3,4]]) // GiNaC matrix
v := lvector_t([1,2,3]) // LAPACK vector
(C^t*C)^(-1)*matrix(v) // Explicit conversion to a GiNaC vector

Operators

Operators arise as a solution to the demand for mathematical notational conventions, like for example infix or post-fix notation, indexed element access of container structures, or assignment to non-variables.

a+b^2 // Means operator+(a,operator^(b,2))

Operators can be overloaded to define simple handling of complex expressions, like matrix inversion, transposition, or conjugation.

You can define your own operators using the register_operator-method of the ContRap Parser class.

The ContRap parser offers a wide set of built-in operators, which are summarized in the appendix A table.

Classes

ContRap is object oriented. This is mainly done to export complete C++ classes. ContRap does not differ between imported classes and classes created using the ContRap language. Imported classes are native ContRap classes with imported functions.

A class definition starts with a ''class''-keyword followed by the class body, which is always a block:

class begin
  local a // Local class member definitions
  ...
  local f := function(...) ... // Local function definitions
end

A class is a regular ContRap object and can be associated with a variable name. An instance of such a class is created by calling the class constructor, which is a local function with the same name as the class

A := class begin
  local a // Local class member definitions
  ...
  local A := function(x:Integer) a := 2*x // Constructor
  local get := function() a // Accessor to the local member
end
...
a := A(2) // The variable ''a'' points then to an instance of the class ''A''

Access to class members is limited to function calls. To access a member function of a class the selector statement is used. We continue with the above example.

x := a:get() // Returns 2
a:a // Throws an error

Logging messages and error handling

ContRap uses its built-in logger facility to perform user-level messaging and error handling. On systems with a console it is, of course, possible to use the console and the error output streams (e.g. printf) for logging. A more elegant way is described in this section.

Writing comments in the user-level mode is done with the ''comment'' and the ''warning'' commands. Both have an optional second argument, which is an abstract name of the sender.

comment("This is a comment","My procedure")
warning("This is a warning","Other procedure")

The logger can be configured to filter warnings or comments with speceific senders.

As already mentioned, there exists an ''abort'' statement, which terminates a function call as if the atomic function was not called.

f := function(x:Integer) if (x = 2) abort("Error") else x*x
f := function(x) x+x
f(3) // Returns 9, since the first function is more suitable for the argument
f(2) // Returns 4, since the first function throws an abort

As opposed to the ''abort''-function there exists an ''error''-command. This command is called, when the arguments of a function are illegal and the execution of the program must be terminated.

f := function(x:Integer) if x = 2 then error("Error") else x*x
f := function(x) x+x
f(3) // Returns 9, since the first function is more suitable for the argument
f(2) // Breaks with an error

Comments and warnings are ''soft'' logging messages. They do not affect the control flow. Abort and error messages are ''hard'' errors, signalizing potential faults. Abort signalizes the interpreter that the arguments of the function are not useful for the execution of the function body. Abort causes the interpreter to look for a different function to call. Error is an evidence of a heavy system fault. It causes the interpreter to quit the execution of the current command disregarding how many commands follow etc. It is comparable to the C/C++ exceptions.