DPROG is a domain specific language for specifying dynamic programming algorithms; given a recursive definition of the problem, the compiler generates code for solving the problem using dynamic programming.
This tutorial/manual explains the basics of the language and how the generated code can be used in an application. The manual covers version 0.2 of DPROG; the language might change in later versions.
The latest version of DPROG can be downloaded from here.
DPROG is released under the GNU GENERAL PUBLIC LICENSE (GPL) version 2.
Dynamic programming is a simple yet powerful technique for solving optimisation problems. When the problem at hand can be split in smaller problems, such that the smaller solutions of an optimal solution are themselves optimal, dynamic programming can be used to avoid re-calculating solutions to shared sub-problems.
Simple problems are both easily specified and easily implemented, but for complex problems translating the specification of the problem into the implementation of the dynamic programming algorithm becomes tedious and error prone. The goal of DPROG is to alleviate this by automatically translating the specification of the problem into an implementation of the solution.
The DPROG language is designed to be close to the ``mathematical'' notation used for expressing recurrences, thus making it easier to specify the problem. Using the DPROG compiler, the manual implementation step can be completely avoided.
This tutorial and manual should give you an idea of the DPROG language and how the DPROG compiler and its output code can be used for writing an application. I will briefly discuss a few implementation details where knowing these could be helpful. (The current version of DPROG has a number of limitations that will hopefully be removed in later versions, but knowing a bit about the implementation you can get around many of the limitations).
To give the flavour of DPROG we first look at a few small examples. We give a detailed description of the language and compiler in later sections.
The classical dynamic programming example is the Fibonacci numbers. The Fibonacci numbers are defined as
F0 = 0, F1 = 1, Fi = Fi-1 + Fi-2 when i >= 2
Calculating Fn is a matter of calculating Fi for all 0 <= i <= n using the recurrence above, and avoid recalculating Fis by storing them in a table.
Expressed in DPROG, using a table called F this is written as:
F[i] = select {0 when i = 0, 1 when i = 1, F[i-1]+F[i-2] when i >= 2}
where 0 <= i <= n
If you store this recursion in a file fib.dprog you can compile it into a C++ function using the command:
shell% dprog fib.dprog dprog_fib
The first argument to dprog is the input file-name; the second is the name to give the generated function (if the name is not specified a default, fill_table, will be used; if the input file is not specified, dprog will read from stdin).
Running dprog with these two arguments will create two C++ files, dprog_fib.cc and dprog_fib.hh. dprog_fib.cc contains the function and dprog_fib.hh the prototype.
The prototype for the generated function looks like this:
void dprog_fib (int F[], int n);
It is the callers responsibility to allocate F to have the needed size; in this case at least n+1 ints (for elements 0 to n).
A small program using the generated function could look as follows (fib.cc):
#include "dprog_fib.hh"
#include <cxx_dprog.hh>
using namespace dprog;
#include <iostream>
using namespace std;
int
main (int argc, const char *argv[])
{
int n = 10; // use default
if (argc > 1) n = atoi(argv[1]); // or read as arg.
int F[n+1];
dprog_fib(F, n);
cout << "Result: " << F[n] << endl;
return 0;
}
The header file <cxx_dprog.hh> is part of the DPROG runtime library as is the namespace dprog.
We can compile it all into an executable, fib using, e.g., g++:
shell% g++ fib.cc dprog_fib.cc -o fib -lcxx_dprog
The library -lcxx_dprog is the runtime library for dprog. (Currently it is not actually needed--all the runtime library is found in the cxx_dprog.hh header file--but you might as well star linking with it. It will contain stuff later.)
Another example is the partition problem from The Algorithm Design Manual; Steven S. Skiena Sect. 3.1.2. Say that we are given an array A of length N and the task of partitioning A into K or fewer sub-arrays such that we minimise the maximal sum of entries in the sub-arrays. That is, we need to find indices i0 < i1 < ... < iK-1 such that the maximal sum A[ij] +A[ij+1] +A[ij+2]+ ...+A[ij+1], for 0 <= j < K, is minimal.
Let M be a table such that M[n,k] contains the minimal cost over all partitionings of the prefix A[0..n] of A into k sub-arrays. The value we are looking for is then M[N,K].
Notice that M[n,k] can be defined in terms of the optimal partitionings of prefixes A[0..i] of A[0..n] (1 <= i <= n) and the cost of the last sub-array A[i..n].
M[n,k] = min{ max{ M[i,k-1], sum{ A[j] where i <= j < n } }
where 1 <= i <= n }
When k is 1 there is only a single sub-array, so M[n,k] is then the whole sum
M[n,k] = sum{ A[i] where 0 <= i < n } when k=1
When n is 1 we are only looking at a single element in A, so M[n,k] is then the value A[0].
M[n,k] = A[0] when n=1
Combining the basis cases and the general case we get the following expression for M[n,k]:
M[n,k] = select{A[0] when n=1,
sum{ A[i] where 0 <= i < n } when k=1,
min{max{M[i,k-1], sum{ A[j] where i <= j < n } }
where 1 <= i <= n } }
where 1 <= n <= N and 1 <= k <= K
Assuming this expression is stored in the file part.dprog we can compile it using:
shell% dprog part.dprog dprog_part
We can use the generated code in a program as this (part.cc):
#include "dprog_part.hh"
#include <cxx_dprog.hh>
using namespace dprog;
#include <iostream>
using namespace std;
static int A[] = {
10, 2, 30, 1, 7 // test array
};
int
main (int argc, const char *argv[])
{
int N = sizeof(A)/sizeof(int);
int K = 2;
Matrix2 M(N+1,K+1);
dprog_part(M, N, K, A);
cout << "Result: " << M.cell(N,K) << endl;
return 0;
}
The Matrix2 class is part of the DPROG runtime system. It is a two-dimensional matrix and is declared in <cxx_dprog.hh>. Here we declare it to be an (N+1)*(K+1) matrix. (We need the +1 on both dimensions because we address up to M[N,K]). We access M[i,j] by the method cell as M.cell(N,K).
After studying the example above you probably already have a good idea of how DPROG programs are written. In this section we look at a more formal description of the language. By looking at the language at a higher abstraction level, as we do in this section, you should get a better feeling for the structure of DPROG programs. Afterwards you can use this section as a reference manual for the language.
| program | := | update+ where range_expression+ |
| range_expression | := | start [< | <=] index [< | <=] stop |
A DPROG program consists of a number of ``updates''--which are assignments to matrices such as the ``M[n,k] = ...'' in the example above--and a specification of the ranges the indices in matrix (n and k here) runs over.
The range expressions are separated by the keyword and as in the partitioning example:
M[n,k] = select{...} where 1 <= n <= N and 1 <= k <= K
Different updates are not separated by and, so you write:
X[...] = ... Y[...] = ...
rather than
X[...] = ... and Y[...] = ...
The start and stop parts of a range can be any expression:
| expr | := | id |
| | | expr binop expr | |
| | | - expr | |
| | | ( expr ) | |
| | | fun_call | |
| | | matrix_lookup | |
| binop | := | + | - | * | / |
| fun_call | := | id ( expr* ) |
| matrix_lookup | := | id [ expr+ ] |
The expressions works as you would expect--if you think the way I do, anyway. But notice that the argument list for a function call can be empty while the index list of a matrix lookup cannot.
The expressions used as arguments to functions or indices into matrices are separated by commas:
f(exp1 , exp2 ,..., expn) M[exp1 , exp2 ,..., expn]
A function call in an expression is different from the builtin functions, such as ``min {...}'' and ``select {...}'', we describe below. Syntactically the difference is in the parenthesis ``(...)'' versus braces ``{...}'', semantically a function call expression is a call to a function defined in your application code, while a builtin function is part of the DPROG program.
Perhaps it helps to think of the builtin functions as statement blocks from structured programs rather than functions. They contain the code for calculating a value--as do blocks--and do not hide the implementation behind an interface--as do ``ordinary'' function calls.
You can also think of the two kinds of functions as basically the same; except the some functions take a tuple of values as arguments while other take a block of code. In that case the general form of a function call is ``id arg''.
In this manual I'll still treat them as different kinds of animals. I will need to describe the builtin functions because they are new to DPROG while ``ordinary'' function calls are well-known.
I'm sorry about the confusion about the kinds of functions here.
| update | := | type? id [ index+ ] = function |
| | | type? id [ index+ ] = expr |
An ``update'' is an assignment to a matrix cell. The matrix cell is indexed using one or more indices. The value stored in the cell is the result of evaluating a function (see below) or evaluating an expression.
The Fibonacci example above shows an update where the value is calculated as an expression (F[i] = F[i-1]+F[i-2]). The partitioning example calculates the value using a function (M[n,k] = select {...}).
Optionally, the matrix can be defined to be of a given type. Currently the possible types are char, int, and float. If no type is given, the matrix is assumed to be of type int.
Thus, the following code assumes that A is an integer matrix:
A[...] = ...
while this code assumes that B is a matrix of floating point numbers:
float B[...] = ...
Implementation Details: The DPROG types char, int, and float are implemented in C++ as char, int, and double respectively. That is, char and int are translated directly into char and int, but I assume that if you want floating point numbers you want the precession of doubles. This might change in the future, so it will be possible to specify both float and double matrices, but at this point I am not convinced of the need for float matrices.
| function | := | id { [when_expr+ | where_expr] } |
A function (or builtin function) evaluates a block of code in a scope identified by some name. The scopes are ``min'', ``max'', ``sum'', ``prod'', and ``select''.
The ``select'' scope is special in that it only accepts ``when'' expressions in the block. I will return to ``select'' when I describe ``when'' expressions.
The remaining scopes all behave in essentially the same way: They take a set of values, given by the expression(s) in the block, and combine them into a new value. In the ``sum'' scope the combination of the values is the sum of the values (well, duh) with a default of 0 if there is no values returned by the expression. With the ``min'' scope the combination is the minimal value, with a default of +infinity. With ``max'' the maximum of the values, with default -infinity, and with ``prod'' the product of the values, with default 1.
Implementation Details: The minus infinity and plus infinity are implemented as INT_MIN and INT_MAX respectively, so if you are not careful you risk overruns in arithmetic expressions using these values. I hope to fix this later, when I come up with a nice design for the fix.
Notice that the block can contain a list of one or more ``when'' expressions or a single ``where'' expression. The use of ``when'' and ``where'' expressions in blocks is not symmetric. The reason is that while a ``where'' expression can provide a set of values to the scope, a ``when'' expression can provide zero or one value. Thus we need several ``when'' expressions to provide a set of values to the function.
| when_expr | := | function when boolean_expr |
| | | function | |
| | | expr when boolean_expr | |
| | | expr | |
| boolean_expr | := | expr bin_rel expr |
| | | boolean_expr and boolean_expr | |
| | | boolean_expr or boolean_expr | |
| | | ( boolean_expr ) | |
| | | ! boolean_expr | |
| | | true | |
| bin_rel | := | = | != | < | <= | > | >= |
A ``when'' expression is a function or an expression qualified with a boolean expression. If the boolean expression evaluates to true, the expression is evaluated and the value is provided to the surrounding scope.
The true boolean expression is used for including values that should always be given to the surrounding scope. Omitting the ``when ...'' from the ``when'' expression is a short-cut for ``when true''. There is no false boolean expression--there is no need for specifying a value that will always be ignored.
As I mentioned, the ``select'' scope is a bit special. It only accepts ``when'' expressions and, in contrast to the other function scopes, it does not combine a set of values. Instead it evaluates the list of ``when'' expressions, on the order provided, and return the first value obtained. That is, the first ``when'' expression for which the boolean expression evaluates to true determines the value of the ``select''. It is an error if no value is provided to ``select''.
The ``select'' function is mainly used for separating the basis cases from the general case in the recurrence. We did this with the Fibonacci example:
select {0 when i = 0, 1 when i = 1, F[i-1]+F[i-2] when i >= 2}
and with the partitioning example:
select{A[0] when n=1,
sum{...} when k=1,
min{...} }
The last ``select'' is also an example of an implicit ``when true''; the last ``when'' expression in the expression will always return a value, so unless any of the two basis cases return a value, it will provide a value to ``select''.
| where_expr | := | function where range_expression+ |
| | | expr where range_expression+ |
A ``where'' expression evaluates a function or an expression over one or more ranges, and provide all the values to the surrounding scope. Thus the following expression finds the minimal element in vector X of length n:
min{ X[i] where 0 <= i < n }
while the following expression
min{ sum{ A[i,k]+B[k,j] where 0 < k < m }
where 0 <= i < n and 0 <= j < l }
findes the minimal cell in the matrix product of the n*m matrix A and the m*l matrix B.
The last expression nests two ``where'' expressions, one calculating the value at (i,j) of matrix A*B, and one finding the minimal over all these. The inner ``where'' expression has a single variable, k, ranging from 0 to m; the outer ``where'' expression has two variables, i and j, where i ranges from 0 to n and j ranges from 0 to l.
I lied above, when I said that a DPROG program consists of a list of updates followed by a list of range expressions. In fact, it consists of a list of global symbol declarations, a list of parameter declarations, and then the updates and range expressions. It's just that the ``globals'' and ``parameters'' can be empty.
| real_program | := | globals parameters in program end |
| | | program | |
| globals | := | globals decl+ |
| | | /* emtpy */ | |
| parameters | := | parameters decl+ |
| | | /* emtpy */ | |
| decl | := | val id : type |
| | | fun id ( arity ) : type | |
| | | matrix id [ arity ] : type |
The ``globals'' and ``parameters'' declarations allows you to explicitly declare symbols (values, functions, or matrices) as belonging to the global scope of your program or as parameters to the generated function.
Symbols that are declared global must be declared somewhere in the application that uses the generated function; they are then linked with the generated code. Symbols declared as parameters must be passed to the generated function when it is called.
For value symbols you declare the type of the value, for matrices and functions you declare the arity (the number of arguments/indices) and the type. For matrices, the type is the type of the cells in the matrix; for functions the type is the return type.
The arity and return type of functions is not enough type information for functions, but in the current version that is all you have. All parameters to functions are assumed to be of type int.
Implementation Details: The reason for the too simplistic view of function types is that explicit declarations have only recently been added to DPROG. Before that I only had implicitly declared symbols, and with the current type check I do not have enough type information to successfully infer function types. This has high priority on the TODO list.
We did not use explicit declarations in the examples above, because DPROG permits implicit declarations of symbols. If an unknown matrix is seen, it is assumed to be a parameter to the generated function. Thus, the Fibonacci program
F[i] = select {0 when i = 0, 1 when i = 1, F[i-1]+F[i-2] when i >= 2}
where 0 <= i <= n
is equivalent to:
parameters
matrix F [1] : int
val n : int
in
F[i] = select {0 when i=0,
1 when i=1,
F[i-1]+F[i-2] when i>=2 }
where 0 <= i <= n
end
Symbols in the start and stop part of the outer-most range expressions--the ones following the list of ``updates''--are implicitly assumed to be parameters as well. Hence, the program above is also equivalent to:
parameters
matrix F [1] : int
val n : int
in
F[i] = select {0 when i=0,
1 when i=1,
F[i-1]+F[i-2] when i>=2 }
where 0 <= i <= n
end
The range variable, i, is not assumed to be a parameter. The range variable in a range expression is explicitly defined in that expression.
If, in the Fibonacci program, we wanted F to be globally defined, but n still a parameter, we would instead write:
globals
matrix F [1] : int
parameters
val n : int
in
F[i] = select {0 when i=0,
1 when i=1,
F[i-1]+F[i-2] when i>=2 }
where 0 <= i <= n
end
It is only the symbols in the outer-most range expressions that are implicitly declared. Value symbols in other expressions, range expressions or other expressions, need to be explicitly declared. Thus, both of the following programs result in type errors:
X[i] = a*i where 0 <= i < n
X[i] = min{ X[j] where m <= j < i } where 0 <= i < n
The first is an error because a is undefined in the expression. The second because m is undefined in the range expression, and it is only the outer-most range expressions that permits implicit symbol declarations.
An unknown function (an expression function, not a builtin function) is implicitly assumed to be global. The following program:
X[i] = f(i) where 0 <= i < n
is equivalent to this:
globals
fun f(1) : int
in
X[i] = f(i) where 0 <= i < n
end
Implementation Details: Currently functions cannot be declared as parameters. This is mainly due to laziness on my part, I haven't gotten around to adding it to the code generator. I will, as soon as I need a function parameter.
What can you refer to, when filling in a cell in a matrix update? Or put another way, which matrix cells are guaranteed to be initialised when the program has reached a certain point?
As a rule of thumb, you shouldn't refer to values that haven't been calculated but will be computed in the future. (Sometimes you can get away with this, by doing some fix-point calculations, but it is unlikely that DPROG will ever reach the level of sophistication where it will help you with that).
So what can you safely refer to? The generated code fills in matrix cells lexicographically ordered by the indices in the outermost range expressions. That is, in the program
X[i,j] = expr where 0 <= i < n and 0 <= j < m
the matrix X will be filled in one row at a time. For each value of i, variable j runs through its entire range. It is therefor safe for expr to refer to X[i',j'] where either i'<i or i=i' and j'<j.
When the program contains several updates, the matrices are updated in the order they are written. Thus, in the program
X[i,j] = expr_x y[i,j] = expr_y where ...
the cell X[i,j] is updated before the cell Y[i,j]. Both expr_x and expr_y can refer to X and Y cells lexicographically under (i,j), but where expr_y can refer to X[i,j], expr_x cannot refer to Y[i,j].
Implementation Details: In future versions of DPROG the compiler will perform some dependency analysis of the expressions to determine how different updates depend on each other. This will remove the restriction that expressions can only refer to matrix cells lexicographically under the current cell.
Now that you've seen all the constructions in the language, it is time for another example. This example is take from bioinformatics and is concerned with alignment of sequences.
Given two sequences, a and b, e.g. DNA or protein sequences, we want to find the best way of aligning them. Here aligning means matching indices of the sequences where the sequences has the same character (or similar characters, for some measure of similar). We must preserve the order of elements in the two sequences, but we can insert gaps in the sequences to move the indices to obtain better alignments. We pay a cost whenever we insert a gap or match two different characters.
Assume that a=GATTACA and b=GTTACA. A good alignment can be achieved by inserting a gap in b between the first G and T:
a = GATTACA b = G-TTACA
If a=GATTACA and b=GATTCCA we can instead allow a mis-match:
a = GATTACA
b = GATTCCA
*
We assume that we have a score function score specifying the cost of aligning two characters. It should reward similar characters and penalise different characters.
In the simplest version of the problem, we assume that the gap-cost is linear, that is, if a single gap cost gap_cost, then n gaps cost n*gap_cost.
We want to maximise the score of the alignment, and we can do this by maximising alignments of prefixes of a and b, and the selecting the optimal next choice of match or gap in either of a and b as follows:
globals
fun score(2) : int
parameters
val gap_cost : int
matrix a[1] : char
matrix b[1] : char
val len_a : int
val len_b : int
in
int D[i,j] = select {-i*gap_cost when j = 0,
-j*gap_cost when i = 0,
max {D[i ,j-1] - gap_cost,
D[i-1,j ] - gap_cost,
D[i-1,j-1] + score(a[i-1],b[j-1])} }
where 0 <= i < len_a and 0 <= j < len_b
end
Here D[i,j] contains the best alignment of a[0..i] and b[0..j]. In the general case, D[i,j] is assigned the best choice of aligning the last two characters, inserting a gap in a, or inserting a gap in b. In the basis cases we either start with a number of gaps in a or in b (with a score of 0 for D[0,0]).
When the algorithm has run to completion, the optimal alignment can be found in D[len_a-1,len_b-1].
We can compile it into the function lin_align and use it as this:
#include <cxx_dprog.hh>
#include <iostream>
using namespace dprog;
using namespace std;
#include "lin_align.hh"
int score(int x, int y)
{
// made up cost function -- no biological meaning
if (x == y) return 10;
else return -1;
}
int
main (int argc, const char *argv[])
{
#include "read-input.cc" // reads `a' and `b' and sets len_a and len_b.
int gap_cost = 3; // made up gap cost
Matrix2<int> score_matrix(len_a+1, len_b+1);
lin_align(gap_cost, a, b, len_a+1, len_b+1, score_matrix);
cout << "alignment score of \"" << a
<< "\" and \"" << b
<< "\" is " << score_matrix.cell(len_a,len_b)
<< endl;
return 0;
}
The +1 on the lengths is because we index from 0 in the C++ arrays, but in our algorithm we're off by one. Alternatively, we could use <= len_x in the algorithm instead of < len_x. We would still be off by one when indexing into the arrays, though.
We use the following code to read in a and b (read-input.cc):
if (argc != 3)
{
cerr << "ERROR! Usage: " << argv[0] << " a b" << endl;
exit(2);
}
char *a = const_cast<char*>(argv[1]);
char *b = const_cast<char*>(argv[2]);
int len_a = strlen(a);
int len_b = strlen(b);
We cast a and b because the score function expects non-const char* arguments (currently you cannot specify a const value in DPROG).
We will use this code again later.
A linear gap cost is usually too simple for calculating a good alignment, and often an affine gap cost is used instead. With an affine gap cost, you pay an initial cost for opening a gap and another cost each time you extend it. So the cost of an n long gap is start+n*continue.
The dynamic programming solution to affine gap cost alignment is to use three tables, A, B, and C. Cell A[i,j] contains the optimal score for alignments of a[0..i] and b[0..j] where the last chararacters are aligned; cell B[i,j] contains the optimal score for alignments of a[0..i] and b[0..j] ending with a gap in b; and cell C[i,j] contains the optimal score for alignments of a[0..i] and b[0..j] ending with a gap in a.
In DPROG we can code this as
globals
fun score(2) : int
val minus_infty : int
parameters
val start : int
val cont : int
matrix a[1] : char
matrix b[1] : char
val len_a : int
val len_b : int
in
A[i,j] = select {0 when i=0 and j=0,
minus_infty when j=0,
minus_infty when i=0,
max {A[i-1,j-1] + score(a[i-1],b[j-1]),
B[i-1,j-1] + score(a[i-1],b[j-1]),
C[i-1,j-1] + score(a[i-1],b[j-1])} }
B[i,j] = select {minus_infty when j=0,
-(start+cont*j) when i=0 and j>0,
max {A[i,j-1] - (start+cont),
B[i,j-1] - cont,
C[i,j-1] - (start+cont) } }
C[i,j] = select {-(start+cont*i) when j=0 and i>0,
minus_infty when i=0,
max {A[i-1,j] - (start+cont),
B[i-1,j] - (start+cont) ,
C[i-1,j] - cont } }
where 0 <= i < len_a and 0 <= j < len_b
end
In the general cases, there are three options to maximise for each matrix, one for each possible choice of the next to last alignment, i.e., one for each of the three matrices. In the basis cases, A contains a 0 in (0,0), B has a row of gap-costs, and C has a column of gap costs. A constant, minus_infty (minus infinity), is used for cells that shouldn't contribute to the calculated score.
When we have run the program, the value we want is the maximal of A[len_a-1,len_b-1], B[len_a-1,len_b-1], and C[len_a-1,len_b-1].
Compiling the code into a function affine_gc_align we can use it as follows:
#include <cxx_dprog.hh>
#include <iostream>
using namespace dprog;
using namespace std;
#include "affine_gc_align.hh"
int minus_infty = -5000; // This can't be INT_MIN because
// will give problems with overrun
int score(int x, int y)
{
// made up cost function -- no biological meaning
if (x == y) return 10;
else return -1;
}
int
main (int argc, const char *argv[])
{
#include "read-input.cc"
// made up values -- no biological meaning
int start = 5;
int cont = 2;
Matrix2<int> A(len_a+1, len_b+1);
Matrix2<int> B(len_a+1, len_b+1);
Matrix2<int> C(len_a+1, len_b+1);
affine_gc_align(start, cont, a, b, len_a+1, len_b+1, A, B, C);
cout << "alignment score of \"" << a
<< "\" and \"" << b
<< "\":\n"
<< "A: " << A.cell(len_a,len_b) << '\n'
<< "B: " << B.cell(len_a,len_b) << '\n'
<< "C: " << C.cell(len_a,len_b) << '\n'
<< endl;
return 0;
}
This concludes our tour of DPROG. Hopefully you now have an idea about how to write DPROG programs and how to use the generated code in your applications.
Version 0.2 of DPROG is the first release, and in some sense it is only an alpha release. Although I believe that this version can be used for more than simple toy projects, I must also admit that it requires a bit of hacking to use it.
I am aware of a number of features I would like to add in future versions, and I am convinced that there are essential features missing that I just haven't thought of yet. These will be found and added to the language as DPROG is put to use on more problems.
Of the missing features I am aware of, and that I plan to include in coming releases, a better handling of function types and dependences between matrices is high on the TODO list.
Also high on the list is support for Hirschberg's algorithm to reduce memory usage in the generated code. I need to think a bit on how to do this first, though. Right now I don't have much of an idea on how to approach it...
Currently the only back-end language supported is C++. Support for, at least, C and Java is also planned. Work on new back-ends will not begin before I am more confident in the input language, however. I want to make sure that we can actually specify the kinds of problems we are interested in before I implement the code generators.
If you have comments or questions on DPROG, feel free to contact me. The same goes for bug-reports and feature requests (although I make no guaranteed that I will add your features, I will consider them).
If you choose to use DPROG for a project, please write me a mail and tell me about it. Right now, DPROG is tailored toward my use; I need input from others to improve it to be generally useful.
Time-stamp: "2003-10-03 23:16:01 mailund"