Dromedar is a statically and strongly typed safe programming language.
As of right now, the language includes primitive types, strings and arrays, fully connected to the garbage collector. Because I am currently working on the language, it is likely that programs that work on day X stop working or change their behaviour due to a compiler update.
The Documentation contains extensive documentation, including a formal grammar and typing specification. The Examples folder contains some example programs with functionality that is already implemented.
I created the Turtle Graphics Module with SFML, which is free to use under the ZLib license.
In order to compile the compiler, you will need clang
, make
and ocaml
installed.
You can then compile the compiler and all the resources it requires with make all
.
Then, you can compile Dromedar source files using ./droml <FILENAME>
. You can specify the output file with -o <FILENAME>
. -l
prints the lexer output, -p
the parser output and -ll
prints the generated LLVM IR code. -s
generates Assembly output, -c
generates LLVM output and writes it to the output file (instead of a fully compiled and linked executable).
Thus, e.g. ./droml ExamplePrograms/Primes.drm -l -o Primes.s -s
writes the clang
-generated Assembly code to Primes.s
, while printing the output of the lexer to the console.
The language also features a module called Turtle
for creating turtle graphics. It uses the Simple and Fast Multimedia Library (SFML), which, in turn, is built on OpenGL.
The way SFML is installed depends very much on the operating system - I have only tested it on my machine which runs Ubuntu Linux 20.04. Building the library works as follows:
sudo make setup-edulib
make edulib
The first command need only be run once - it installs SFML to the device. The second builds the C/C++ files linked to the executable Dromedar programs.
The Config File contains two drmlibs
and two cpplibs
items, depending on whether you want to build a turtle graphics application or not. The first line for each identifier counts, so you need to swap them around to match your wishes for the application.
After you've done that, you can use the Turtle
module without having to specify any additional compiler information when you compile source programs using droml
.
This file is an example program which uses turtle graphics.
It is possible to write extensions to the Dromedar standard library - both in Dromedar and in C (the language in which most of the standard library is implemented) - or even in C++ (which is a slightly more involved process).
In order to expand the standard library with functions written in pure Dromedar, you will either need to create a new .drm
file in which to create your functions, or append them to the existing standard libary file. If you create a new file, you need to add its path to the drmlibs
item in the Config File.
You can then use all the functions you created in your own Dromedar programs.
Writing native functions requires the following steps:
- Write the function signatures in a
.drm
file (as if you were to expand the standard library in Dromedar - see the section above). For this, you need to createnative
functions, types and values. Native types are used by the Dromedar runtime system to hide functionality of other languages behind them (e.g. astd::regex
object as used by theRegex
module of the standard library). They are treated like blackbox types by Dromedar programs. - You can then take a look at the LLVM output of any Dromedar program. Note that attempting to compile it to an executable now will most likely lead to a linker error since it cannot find the functions referred to by your signatures. The LLVM output will contain function declarations for your newly created functions. These are quite similar to how you will have to implement them in C.
#include "cutils/common.h"
will give you access to some typedefs that make your code look similar to the LLVM declarations. - You now have to compile your new file(s) - either compile them by hand by following the
stdlib
rule in the makefile or by adding them to the task list in saidstdlib
rule.
You must make sure that the compiled .o
file is located in the obj folder. Then you can use your new C functions in Dromedar using your native function signatures.
Note that if you return reference objects (including blackbox native types), you need to allocate them using _allocate(int64_t)
- the function defined in the garbage collection header file.
Sometimes, writing functions in C++ is a considerable reduction in complexity compared to C. If you want to write a C++ extension to the standard library, you will still have to complete the steps for a Dromedar extension and the first two steps for a C extension.
Then, you can create a C++ header and source file (let's call them l.h
and l.cpp
) in which you will implement the core functionality of your new functions. You have to call the C++ functions from the C file which contains your functions that match the LLVM signatures.
In order to make the C++ functions interoperable with the C native functions, you need to make your files look as follows:
l.h
#ifndef __L_H__
#define __L_H__
#ifdef __cplusplus
extern "C" {
#endif
/*
* your C++ function headers
*/
#ifdef __cplusplus
}
#endif
#endif
l.cpp
#include "l.h"
/*
* static C++ functions
*/
extern "C" {
/*
* implementations of your C++ headers from l.h
*/
}
A good example of how your C++ and C files must look is the Regex module of the standard library: Regex.c, Regex.h, and Regex.cpp.
You can then either compile your C++ file to a statically linkable library .so
file by following the execution steps of the stdlib
rule in the Makefile, or add it to the taks list in the rule, following the outline given by the rule in the makefile.
Following that, make sure your .so
file is located in the obj folder. Then, you must the path to your library in the Config File, following the example of the C++ libraries that are already there.
After that, you can freely use your new functions in Dromedar programs.
Everything described in this section is described in more detail in the documentation. This text simply serves as a quick introduction into the language and its features.
fn main -> void
IO.print_str("Hello, World!\n")
Each Dromedar program consists of a series of global function and variable declarations. Function bodies contain statements (assignments, variable declarations, if-statements, etc.).
The language uses significant whitespace. Two neighboring instructions in the same block require the same indentation string, whereas a deeper instruction requires a longer indentation string. Two blocks with the same indentation level do not necessarily need the same indentation string, but they do need to match their respective environments.
You can separate instructions on the same line with a semicolon, and it is possible to write a single instruction on multiple lines, provided you use a deeper level of indentation than the depth for the first line.
Function declarations are created using the fn
keyword, followed by a name, an argument list and a return type. For example, the function header fn f (x:int, y:int) -> int
declares an int
-function, taking two int
arguments.
Functions are accessible in the entire program, meaning that mutual recursion is possible without forward declarations.
Dromedar knows four primitive types: bool
, char
, int
and flt
.
The language uses a safe typing system. This means that null
pointer errors are impossible: dereferencing a reference object is only possible if it is of a strictly non-null
type. Reference types come in non-null
, and maybe-null
types, which are denoted by a ?
suffix.
There are three ways to write down list literals:
- As an actual list:
[1,2,3,4,5]
is the list containing the numbers from 1 to 5. - As a bounded list:
[1...5]
is the same list. The delimiter..|
doesn't include the last element,|..
doesn't include the first, and|.|
doesn't include either. Thus,[1...5]
and[0|.|6]
are equivalent. Bounded lists can only returnint
lists as of now, but I plan on extending them tochar
lists as well. - As a comprehension list:
[exp : decls : condition]
, e.g. as follows:[2*x : x in [0...10] : x > 5]
, which is equivalent to[12,14,16,18,20]
.
Dromedar also knows partial function application, whereby a new function is created by leaving some of the function's arguments unspecified using the _
wildcard operator.
Consider the function add : (int,int) -> int
. Then, add(_,3)
creates a function of type int -> int
which adds 3
to its argument.
The following are simple local declarations:
let a := 3
let b := 2.0
let c:int := 7
let d:flt := 6
let e:string? := "hi!"
let f:[[int]] := [[1,2],[3],[4,5,6]]
The type specification is optional, but it allows for some bending of the rules: Declaring a variable as flt
and assigning it an int
value (and vice versa), is possible - as well as assigning a variable of type t?
a value of type t
, as in the string declaration.
Variables are immutable, unless declared with the mut
keyword, in which they may change the value assigned to them.
There are five control flow constructs implemented as of today: if-elif-else
, while
, do-while
, for
and denull
. Here are semantically equivalent examples for do-while
and for
:
mut i := 0
do
printf("{0}\n", i)
i := i + 1
while i < 10
for i := 0 ..| 10
printf("{0}\n", i)
Both these programs print the numbers from 0 to 9.
The denull
statement checks whether an expression passed to it is null
. If not, its result may be used as if it were of non-null
type, as follows:
denull x := getstring()
IO.print_str(x)
A denull
statement may be followed by an else
block which gets executed if the expression is null
.