Dodo illustration

Dodo for programmers

Welcome to dodo, a programming language that tries to be different

There are many programming languages today, each with unique features and available on a variety of platforms. Dodo aims at exploring new paths, or at least to include features in a combination never seen before.

My intention is not to code away right yet. I would like to start with deciding what should or should not go into the language, with the focus on exploring new and rare ideas.

Other objectives of dodo are

Structure of programs

Dodo programs are organised in a series of text files pertaining to one of the following categories

      __Module__ modulename
__Header__
[...header code...]
__Main__
[...source code...]
__Library__ libraryname (version: "versionnumber")
[...library...]

Syntax of the language

The main ingredients of a program are statements and expressions. At the top level a program contains only declarative statements. Both statements and expressions can be used inside a function block. However a statement cannot be used in place of an expression where an expression is expected. Declarative statements are of the general form:

      Type name[(Type, ...)], ...
Type name[(Type parameter [=value][, ...], ...)] block | =value

Examples of declarative statements are:

      import rand
int x = 36
String format(Pattern, Date)
Date today, tomorrow

The import statement imports declarations from a module or a library into the current context. Conversely, the export statement exports the declaration or module that follows. It is used in library files. Note that loading a module or library (dynamic linking) is not done with import, in contrast with Java.

Several statements can be written on the same line. The separator is semicolon. The semicolon at the end of a line is optional. Multiline statements can only be broken up at specific places, eg. after an opening bracket.

The program can contain multi-line comments using curly braces and dash or single-line comments introduced with the hash character:

      {- Multi-line comment -}
# Single-line comment

Function and type documentation uses the APT (almost plain text) syntax inside multi-line comments.

A block is delimited with C-style curly braces or with a colon and a period. The ending period should always be followed with white space:

      {
# C-style block
}

:
# Dodo-style block
.

Both styles of blocks can be used interchangeably, however an opening brace always closes with a brace and a colon always closes with a period. It is considered good style to use only one style of block in the same file or to alternate one style with the other.

Types

Dodo is a strongly typed language and types play an important role in the language. Dodo is object-oriented, prototype-based and statically typed. It offers various techniques to improve reuse and the separation of concerns.

Type declaration shortcuts

If the declaration has an initial value of the same type, it is not necessary to specify the type. The keyword def is a shortcut that avoids naming the type explicitely, as in:

      def x = 36  #x of type int
def prettyHouse = House(5, blue, white) #prettyHouse of type House

Class declaration

A class of objects describes the properties and operations common to all the objects that are part of it. Additionally it describes how to create an object of this class. The method to create an object is called a constructor and the created object is called an instance of the class.

For example:

      class House
{
Door entryDoor
Window[] windows
Colour roofColour, wallColour

{-
[House]
Constructor for a house

[int windowCount] the number of windows

[Colour roofCol = red] the colour of the roof

[Colour wallCol = white] the colour of the walls
-}
House(int, Colour?, Colour?)

{-
[draw]
Make a drawing of the house

[Canvas paper] the support to draw on

[return(Drawing)] the final drawing
-}
Drawing draw(Canvas)
}

The constructor constructs the object by initialising the class attributes using the provided values or default values. After the attributes are initialised they never change, except for special cases detailed later.

The example above shows a function draw that has a parameter of type Canvas. A function has a return value which depends on the arguments provided, however it normally returns always the same value if it is provided the same arguments.

Note that any attribute, like entryDoor or windows in the example above, can be either a function or a value. That is different from a function with no arguments which is not considered an attribute.

The general syntax for a class declaration is:

      prototype ClassName [= new ParentClass[(attribute: value, ...)]] [is Qualifier, ...]
{
[...links...]
[...attributes...]
[...rules...]
[...constructors...]
[...functions...]
[...methods...]
[...states...]
}

There are several things that need explanation here. The prototype is an object that is used as base to build an instance of the class. In the previous example the prototype was class. You can use def instead if no prototype is required. If you want your class to inherit properties and operations from another class, making effectively a more specialised version of it, specify it as parent class.

A qualifier can be listed to add qualities to the class that are the same for all classes that respond to that qualifier. For example the qualifier Motorised could add the attributes engine, wheels and speed to the class.

Links are similar to attributes but an attribute is part of the object while a link denote a relationship between two classes. For example, the entry door is a part of a house. On the other hand we could add a link between the class House and the class Child, to denote that one or more children inhabit this house.

A constructor is like a function which role is to initialise the attributes of the object. A constructor has no return value.

Methods are similar to functions. But they can modify the attributes of the class and their arguments. Also, a method can have several versions each with a different set of parameters. The language considers methods more like objects than like functions.

Finally, attributes, functions and methods can be grouped inside a state block. The state of the object can vary when a transition occurs.

For example, the house could have for states "inPlanning", "hasFeatures" and "hasColours" which correspond respectively to the project of a house, a house with a door and windows and a house with a door, windows and a colour for the roof and the walls. Then draw would be available for a house in the state "hasFeatures" or "hasColours", while paint would only be available in the latter state.

Class attributes

It is possible to define an attribute or a function that is not specific to a single instance, but rather to a class. The attribute can be overloaded in a subclass. The syntax for a class attribute is the same as for an instance attribute, except that it is preceded with the class name:

      Type Class.name[(Type, ...)], ...
Type Class.name[(...parameters...)] block | =value

A common use for class attributes is to define factory functions. These are like constructors except that they have no side effects and can be used in functions. The convention in dodo is that factory function names start with new and contain a description of the parameters, for example:

      Self House.newWithWindows(int wc, Colour wallCol = white, roofCol = red):
def wind = Window[].newWithSize(wc)
return new instance(windows: wind, wallColour: wallCol, roofColour: roofCol)
.

Here only the first parameter is described in the name because the roof and the wall colours have a default value and are optional. Self is a special type that represents the type of the class (House in that example). It can be used only as return type.

Class attributes are independent of the state. If they are declared outside the block of the class they cannot access any private function of the class.

Generic type

A type template allows to define a generic type. A generic type takes on one or more type parameters to make an effective type. For example a type template for a list could be:

      class template List(itemType: $T)
{
T head
List(<itemType: T>) tail
}

An example of effective List type is:

      def HouseList = new List(itemType: House)
HouseList houses #a list of Houses

Or in short form:

      List(<itemType: House>) houses  #a list of Houses

A generic type can also be built with class attributes, then its parameters are not types.

Type matching

The ~ operator and the test match construct can be used to match a type to a specific type pattern. For example:

      class Building is Abstract
def House = new Building: int House.windowCount; Colour House.wallCol.
def EiffelTower = new Building: meters height() = 325.0.
def School = new Building: String School.name.

Building building = House(<windowCount: 6, wallCol: green>)()

test match (building):
?/House(wallCol: $colour) {return "The "(colour)" house"}
?/EiffelTower {return "The Eiffel Tower"}
?/School(name: $name) {return "The "(name)" school"}
?/Building {return "Any other building"}
.

Inheritance

A class inherits all the attributes, operations and internal classes of its parent class. There is no need to declare them again. In dodo there can be only one parent class.

Qualifiers are inherited except for the Abstract qualifier (by definition).

Because the inherited class has the same interface as its parent class, it can be used anywhere an object of the parent class is expected. This enables polymorphism when the type is Polymorphic.

Each non-abstract class has a default instance. The default instance of the class provides a value for attributes and an implementation for functions and methods. If the prototype is not specified in the class declaration, the default instance of the parent class is used as base.

If an attribute is redefined its type must match exactly that of the same attribute in the parent class. If a function is redefined it must accept all the arguments that the original function accepts and return only values that the original function could return. In practice the types must be the same as the original function, or the parameter type can be a parent class of the original and the return type can inherit from the original.

If a variable was already defined, its name must be preceded with ^ where it is redefined. That assures the compiler that you really want to override the variable. The same applies to functions and methods.

Qualifier definition

A qualifier helps to add specific qualities to various types. For example, if you want to define a Inhabited qualifier which can be applied to the house you could write:

      qualifier Inhabited
{
Link(<to: People[]>) inhabitants
}

Unfortunately, the first version of Inhabited does not know how to draw its inhabitants. This new version overrides the draw function in House to draw the inhabitants in addition to the house:

      qualifier Inhabited
{
Link(<to: People[]>) inhabitants

Drawing ^draw(Canvas paper):
Drawing result = Super.draw(paper)
loop foreach (inhabitant in inhabitants)
{
.result = inhabitant.draw(result) #Drawing can act as Canvas
}
return result
.
}

It is also possible to add instructions before and after a selection of functions using wrap selector. For example, this qualifier prints the function name and the number of arguments:

      qualifier template Tracing(to: $output)
{
console.Output out = output

Tracing():
self.out.Activate()
.

wrap selector (sel):

$function($argument*)
{
console!out.Puts("Calling "(function)" * "(argument.count))
try:
return sel.eval
.
console!out.Puts("Finished "(function))
}
.
}

Dodo allows to add qualifiers to a variable or a type by overriding its declaration. The qualification is effective from its declaration until the end of the current scope. Trying to add an existing qualifier has no effect. Variables which were added qualifiers are a different type from the original variable. The general form of declaration overriding to add qualifiers is:

      def ^variable is Qualifier, ...
|
def ^Type is Qualifier, ...

If the variable or type is not compatible with the qualifier, an error is thrown. Qualifiers can be added to a function parameter where the function is defined.

Properties of type instances

Abstract state

An object can have different properties depending on its state. Instead of relying on a state attribute to decide the outcome of operations, dodo lets you define different states with each its own set of attributes, functions, rules etc. A state can be further split into substates, usually as a result of adding qualifiers or Inheritance.

The default state is the first state declared.

Constructors can be part of a state, but the same constructor cannot appear in more than one state block. If the construtcor is declared outside of a state block, then it is part of the default state.

Attributes and methods that are defined out of a state block are available in every state. The syntax of a state block is:

      __State__ statename [in parentstate]
[...transitions...]
[...links...]
[...attributes...]
[...rules...]
[...constructors...]
[...functions...]
[...methods...]

The transition from one state to the other can be caused by a transition function or a validation rule for state. If the object is Tagged a transition function returns a copy of the object in the specified state. It can have an optional constructor which performs additional operations on the copy and the object. For example:

      Transition(<to: open>) open()

def Close = new Transition(to: closed):
Close()
{
gates.Shut()
.closed.alarm = enabled
}
.

The object takes the value of the copy when the transition constructor terminates. If a transition constructor is called inside a method of the object, that method must be defined in both states. The same applies if the method modifies the state through a validation rule and Validate().

Validation rules

Validation rules can be defined to keep the object in a coherent state. When the attributes of the object are set, the rules are applied before copying the attributes to the object. The syntax of a rule is:

      attribute = expression

The attribute is updated when an attribute that appears in the expression is set. For example:

      rules
{
# Centre automatically
left = (screen.width - width) / 2
right = (screen.width + width) / 2 - 1
width = right - left + 1
}

These rules calculate left and right if either screen or width changes, and width if either left or right changes. On the other hand, left and right can be set independently of each other.

The expression fail() causes the rule to fail with ValidationException. The special rule autovalidate = false informs dodo that rules should not be enforced automatically when attributes are set. It can be set globally:

      rules:
autovalidate = false
.

It can also be set for a single time when deriving a prototype:

      def blueNotes = new notes(autovalidate: true, paperColour: blue, width: 40)

Autovalidation does not occur when an attribute of a Mutable variable is set. To apply the validation rules, use Validate():

      .child.lastName = "Thomson"
child.Validate()

Links

A link denotes a relationship between two or more objects. A link declaration is written:

      Link(<to: TargetType>) toTarget  #one-to-one relationship
Link(<to: TargetType[count]>) toTarget #one-to-many relationship

If count is omitted there can be any number of items in the list.

Access to the linked object is done through toTarget, or toTarget[index] in case of a one-to-many relationship. Operations on the linked object are prefixed with link!, to distinguish from operations on the link itself.

A link can be active or passive, and operations can only be used on an active link. At creation time the link is active. When the link is stored in a separate variable it becomes passive. Use Activate() and Deactivate() to alternate between active and passive status.

You can also set up a bidirectional relationship with Bilink:

      Bilink(<to: TargetType, reverse: toSource>) toTarget
def toSource = toTarget.toSource #link to source

When the target object changes, the reverse link is updated to point to the source object.

A Relationship object can be used as in-between for a relationship between several objects. The relationship behaves like a map between a relation name and an object. For example:

      Relationship hub
.link!hub[toTarget] = target

An existing non-linked object cannot be referenced by a link, but you can assign an existing link value to the link. That way both links point to the same object.

Prototype instantiation

Along this document, you may have noticed in the examples that some types start with a lower case letter and some with upper case. Dodo makes the distinction between lower case and upper case. In fact, this is very much a part of the language. The convention is the following: types starting with upper case are classes. Types starting with lower case are prototypes.

What is a prototype? It is an object that can be used to instantiate other objects. All named objects in dodo are prototypes. By default, instantiating an object from a prototype creates a word-for-word copy of the prototype object. Of course that is of little use, so you can customise your instance using a very similar syntax to classes.

For example:

      def prettyHouse = House(5, blue, white)

def school = new prettyHouse(wallColour: lime)
{
Playground playground
Bell bell

__State__ open
Transition(<to: closed>) close()
method Play: Play(&Child[], Area?)
=>Game played.

__State__ closed
Transition(<to: open>) open()
}

In the first line we create an instance of House called prettyHouse. Now that object can be used as a prototype for the school object, which is just like prettyHouse except that its walls are painted lime instead of white, it has a playground for the children and a bell to ring the classes. Also when the school is open the children can play in the playground.

The general syntax of a prototype instantiation is:

      new prototype[(attribute: value, ...)]
[{
[...links...]
[...attributes...]
[...rules...]
[...functions...]
[...methods...]
[...states...]
}]

Methods and functions

As a reminder, a dodo function is designed to return a value in function of its inputs. It does not have any side effects (there are exceptions). On the other hand methods are allowed side effects and they don't have a return value as such. This chapter describes some advanced topics related to methods and functions.

Multiple dispatch

Normally a function call invokes the function defined in the class of this specific object. However sometimes we really want to look at the type of the arguments of the function to decide what to do. Instead of testing the type of the arguments in the code, you can use multiple dispatch to let dodo do the matching for you.

To use multiple dispatch you define additional functions with the same name, where the type of the parameter is replaced with one of its subclasses prefixed with "^". So if the function is:

      Type function(ParamType, ...)

Another declaration that adds multiple dispatch would be:

      Type function(^Subtype arg1, ...)
{
...instructions...
}

where Subtype inherits from ParamType. If the argument to the function matches Subtype the second declaration will be used, otherwise the first declaration will be used.

For example:

      Drawing draw(Canvas paper):
#Draw with pencils
.

Drawing draw(^Blackboard board):
#Draw using chalk
.

Note that multiple dispatch is particularly useful when the operation varies depending on more than one parameter type. The subtype can also be replaced with special type Self in subclasses or with an interval of values.

Methods and type conversion

An object of a specific type can assume the role of another type if a conversion is defined for it. The general syntax of a conversion is:

      =>Type conversion block | =value

Like functions, conversions cannot use methods and always return the same value. For example, let us define a conversion that will tell a description of the house wherever an instance is used as text:

      def HouseStory = new House
{
{-
[-> tell String]
Converts to a String

A description of the house for the attentive reader.
-}
=>String tell:
String description = "A house with %i windows, a %s roof and %s walls"
return description.sprintf(windows.count, roofColour, wallColour)
.
}

The return keyword is used in functions and conversions to terminate and return a value. It can be used without a parameter in a constructor or a method to terminate it. Methods do not return a specific value; instead, they can provide a conversion that is invoked to retrieve a value. Remember that methods are considered as objects in dodo.

You can assign a value to a conversion in a method. That does not terminate the method.

For example:

      method Play:
Play(&Child[] children, Area area = playground)
{
loop foreach (child in children):
child.PlayWith(children[where self != child], area)
.
.self.played = `all children`.lastGame.mode
}
=>Game played
.

To use that method, you could do:

      Game chosenGame = school.Play(.schoolChildren)

That will make chosenGame hold the most popular game among the children. Note that a conversion needs to be named explicitely if it returns a type compatible with the class type or with the return type of a conversion defined previously. In the example above Game and method are incompatible types, so there is no question whether to use the conversion.

Returning a sequence of values with yield

The special return type yield indicates that the function will return a value several times in sequence. Use loop foreach to retrieve the values. An example of use would be a function that questions all the children for their name and another that notes down the answers one by one:

      yield String question(Child[] children)
{
loop foreach (child in children):
return child.answer("What is your name? ")
.
}

Notes collectNames(Child[] children)
{
Notes notepad
loop foreach (name in question(children)):
.notepad = notepad.write(name)
.
return notepad
}

When a yielding function does return its state is saved in memory and the caller regains temporarily control of the execution until the caller requests the next value. When a yielding function terminates the EndOfYield event is raised.

Another way to get the values yielded by the function is to save the returned generator in a local variable. The resume construct allows to return it the control to generate the next value. This combination provides a convenient way for two functions to work together. For example:

      Notes collectNames(Child[] children)
{
Notes notepad
def askName = question(children)

try:
.notepad = notepad.write("First child: " + resume askName)
.notepad = notepad.write("Second child: " + resume askName)
.notepad = notepad.write("Third child: " + resume askName)
loop foreach (name in askName)
{
String nth = ++notepad.lines.count + "th"
.notepad = notepad.write(nth + " child: " + name)
}
.
return notepad
}

Expressions

In a computer program, most of the work is carried out by expressions. In its simplest form an expression is a variable name or a constant. The simple expression can be preceded with a unary operator. Two simple expressions can be combined with a binary operator. A selector can be appended to an expression that supports it. Function calls are also expressions.

A few examples of expressions:

      -4
colour["red"]
5 * 9
draw(silkPaper)
if (x > 4.0) x else f(x)
.total = 15

The dot unary operator makes a reference out of a variable. This is necessary to allow modification of a variable, in that case total changes to the value 15. Note that a function is not allowed to change an object passed as argument in any way. A constructor or a method can.

In functional programming there are no instructions, only declarations and expressions. Dodo has some support for functional programming, even though it is an imperative language.

Lambda expressions

It is sometimes useful to make a function out of an expression, so that it can be used later to calculate a value. This is called a lambda expression. In dodo a lambda expression is of the general form and type:

      fun (...parameters...) [-> continuation Type, ...] {expression}
Fun(...ParamTypes... -> Type, ...)

For example, a function that counts the number of windows in a green house can be written:

      def greenHouseWindowCount =
fun (House theHouse) -> return int, escape int = default:
if (theHouse.wallColour = green) return(theHouse.windows.count)
else escape(0).

Note that external variables used in the expression are replaced with their value, they are not evaluated again. You can declare a variable local to the lambda expression with expression (-> variable) expression;.

Calling a continuation is optional, when the lambda expression body is evaluated its value is implicitly passed to the first continuation. If the last continuation has a single Exception parameter it is used when an exception is thrown. A continuation can have a default value which is another continuation or default. Inside the lambda expression $variable (or _ if it is anonymous) evaluates to the lambda expression itself so you can use recursion.

The above function can be used to count windows in several houses:

      int w = greenHouseWindowCount(house1)
+ greenHouseWindowCount(house2)
+ greenHouseWindowCount(house3)

You could also take advantage of escape to stop the calculation (and return the sum so far) as soon as a white house is found. This is done using continuations:

      int w = greenHouseWindowCount(house1) -> count1
count1 + greenHouseWindowCount(house2) -> count2
count2 + greenHouseWindowCount(house3)
| ;
| ;

An empty continuation evaluates to the passed value. If the number of continuations in the call is less than the number of continuations declared in the lambda expression, the extra continuations throw an exception. Since greenHouseWindowCount calls return only when the house is green the control flow will stop on a white house. To count the total number of windows in a sequence of green houses you could do:

      def totalWindowCount =
fun (House[] houses):
if (houses.count > 0)
greenHouseWindowCount(houses[1]) -> count
$totalWindowCount(houses[2+]) -> rest
return(count + rest);
| ;
else 0.

The general form of a lambda expression invocation is:

      lambdaExpression(...arguments...) (-> ...variable...) expression (| ...) ... ;

The semicolon is optional at the end of a block or an expression in brackets.

Technically dodo does not use simple lambda expressions but rather CPS (continuation passing style) lambda expressions. That allows greater control of the execution flow. Normal functions use two continuations, return and throw. The following declarations are equivalent:

      String fullName(Child)
Fun(Child -> String, Exception) fullName()

Functions declared inside a class have a different type.

Functional constructs

A previous example introduces the if construct which returns either the first or the second value depending on a condition. It is a functional construct. Dodo has some powerful functional constructs borrowed from functional programming.

To compare a variable with more than a single value use the match construct. This is typically used with the ~ operator to select a result according to a variable. The expression corresponding to the first match is returned. For example:

      selection ~
match /^([0-9]+)/ -> n
getRecord(n)
|
match /date: ([0-9]{2})\/([0-9]{4})/ -> m, y
Record.newWithDate(Date.parse(y + m, "YYYYMM"));;

A note about regular expressions: grouping parentheses, as in (a)* or (a|b), do not produce a captured string with operator ~. You should enclose the expression in another set of parentheses to capture it.

Sometimes one want to apply a function to a list of values instead of a single value. One may want to exclude some items from the list based on a condition. The `all and the where constructs (map and filter in functional languages) are designed for this purpose. The general form of `all and where is:

      `all list`.attribute[(argument, ...)]
|
function(`all list`, ...)

list[where condition]

They can be combined together, for example to get a list with the number of windows in each green house:

      `all houses[where wallColour = green]`.windows.count

One may want to use the result of a function as argument of that same function recursively, using each element from a list as another argument. The `from/`to construct (fold or reduce in functional languages) should be used for that. The general form of `from is:

      `from list`.function(`to initialValue`, ...)
|
function(`from list`, `to initialValue`, ...)

If the initial value is not specified, the first value in the list is used. That requires the list to have at least one element, which needs to be the correct type.

Take note that the function is not called if, and only if, the list is empty (or the list has a single element used as initial value). Instead the initial value is returned. You should chose your initial value carefully taking into account the case of an empty list.

To illustrate the use of `from, let us rewrite the overriding code of Inhabited in functional style. This draws all inhabitants of the house, using the drawing as canvas for drawing the next inhabitant in the list:

      Drawing ^draw(Canvas paper) =
`from inhabitants`.draw(`to Super.draw(paper)`)

The converse of `from is for, which builds a list from an expression. This functional construct is similar to a loop. The general form of for is:

      [for parameter = value, condition, next {expression}]
|
[for parameter in list, ... {expression}]

The default condition is true, and the default expression is the parameter or parameters. In the first form all parameters are set at each iteration; in the second form the next parameter takes all possible values for each value of the first parameter. The terms of the for are evaluated as needed (lazily).

For example to calculate the square numbers from 1 to 5:

      [for x in 1...5 {x ** 2}] #gives [1, 4, 9, 16, 25]

Finally, the `zip construct works in a pair to combine two lists into one. The general form is:

      function(`zip list1`, `zip list2`, ...)
|
`zip list1`.function(`zip list2`, ...)

If the lists have different size the combined list is the size of the shortest.

The converse of `zip is unzip, which builds a pair of lists from two expressions. The general form of unzip is:

      unzip (parameter = value, condition, next {expression1, expression2})
|
unzip (parameter in list, ... {expression1, expression2})

Operations on indexed variables

An additional functional construct is `apply. The expression that follows `apply is analysed to find indexed variables where a non-indexed variable was expected. Then the operations are applied to each element in the list, producing a list of results. For example:

      `apply [2.5, -1.0] * [1.1, 8.2]` #results in [2.75, -8.2]
`apply [child: "Art", parents: ["Gene", "Amanda"]] + " Miller"`
#gives [child: "Art Miller", parents: ["Gene Miller", "Amanda Miller"]]

This is similar to `zip or `all, but it is based on the Normalise-Transpose-Distribute (NTD) concept. This concept was developed for the NASA at Texas Tech University to help write algorithms in a simple way while offering more opportunities for parallelism.

The `apply construct can take an optional function which is applied recursively on the resulting list in similar fashion to `from. For example to calculate a sum:

      int sum = `apply(+) [1, 2, 3, 4, 5]` #gives 15

The arguments and return value of the function must be the same type as the elements of the list. There should be at least two elements in the list for a binary function. The first argument receives the recursion.

Instructions

Instructions are statements that can be found inside functions. They include declarative statements, expressions and branch statements.

Conditional branch statement

The first branch statement is test. There are several forms of test. The first is similar to if in C. It takes a condition, and the instructions in the test block are executed only if that condition is true:

      test if condition
{
[...instructions...]
}

A different form is simply test. It is similar to if... else if... else... in C. In that form the test block contains several alternative conditions, and only the block corresponding to the first condition evaluated true is executed. The special condition default always evaluates to true and is executed if no other condition is true. An exception is raised if no condition in the test block evaluates to true.

      test
{
condition:
[...instructions...]
.

...

[default:
[...instructions...]
.]
}

The last form is test match. That form takes an expression as argument and only the block corresponding to the first pattern that matches that expression is executed. The special pattern default matches any expression and is executed if no other pattern matches. An exception is raised if no pattern matches the expression.

      test match expression
{
pattern:
[...instructions...]
.

...

[default:
[...instructions...]
.]
}

Loop branch statement

The other branch statement is loop. The simplest form is loop, which repeats the instructions inside the loop block indefinitely.

      loop
{
[...instructions...]
}

Another form is loop while, which takes a condition as parameter and repeats the instructions in the loop block only if the condition holds true.

      loop while condition
{
[...instructions...]
}

Another form is loop until, which takes a condition as parameter and repeats the instructions in the loop block until the condition is true.

      loop until condition
{
[...instructions...]
}

A form of loop is loop for. That form takes three arguments: the loop initialisation, the condition and the loop step. If the loop step is missing it defaults to the same as initialisation. This form is similar to the C for loop:

      loop for (initialisation ; condition [; step])
{
[...instructions...]
}

Another form is loop foreach. That form takes as argument a list expression preceded by a variable name and keyword in. The variable takes for value each element of the list in turn. There can be more than one variable in list argument, in which case the next list is looped over for each value in the preceding list.

      loop foreach (variable in expression, ...)
{
[...instructions...]
}

Finally loop can be combined with select or match to behave in a similar way to test or test match, except all cases are tested in turn. The loop finishes when no more condition evaluates true or no more match is found for the expression. The default block is executed only if there is no other match on first iteration.

Exception and error handling

Exceptions are events that do not occur in the normal execution of a program, but may be caught and handled when they do occur in exceptional circumstances. An example of exception is when the program tries to access an array item out of bounds. Dodo provides an exception handling mechanism to manage this.

Event handling block

If the program encounters an edge case (error condition or other), it throws an exception and dodo looks for an event handling block to determine what to do. An event handling block is never part of another block. The event handling block can access variables declared at the location where the exception was thrown from. The syntax of an event handling block is:

      catch ([event: $variable[, at: $label]])
{
[...instructions...]
}

The event variable is of type Exception. It contains information about the exception and its context. The event handling block can contain any instruction. However, if an exception occurs in the event handling block the following instructions are not executed, instead the next event handling block takes control.

An exception can be signalled with the throw instruction. That instruction is similar to return except that it only takes an argument of type Exception. There is an implicit throw instruction at the end of the event handling block. If the exception was treated, you can use return to terminate the function normally or resume to resume the function at a specific point.

To mark a location where to resume from use @location, where location is either a word or a number. For example:

         double bestGuess = (@1 `all children`.guess).average
return "The children guessed: " + bestGuess
catch (at: @1)
{
resume @1 5.0 #resume with 5 if the child did a mistake
}

Try block

Sometimes the function needs to free resources before terminating with or without an exception. For example, an open file needs to be closed. For this purpose, enclose the intermediate instructions in a try block:

      files!scoreFile.Open(read)
try:
return files!scoreFile.ReadInt()
.
files!scoreFile.Close() #always close the open file

The instructions after the try block are executed before the function terminates with return or throw. If neither occur inside the try block the instructions following the try block are executed normally after it finishes. If an instruction is preceded with the special markup __Undo__, it is executed only if an exception occurs inside the try block. The __Undo__ markup allows to revert instructions in case of exception. You can also use resume with it.

If the program encounters return or throw after the try block, the return or throw inside the try block takes precedence over it. The event handling block is considered part of the try block if an exception occurs inside the try block.

You can use a shorthand notation for the try block. If you use the long arrow ...=>, all instructions from the next line till the end of current block are considered part of the try block. The instruction after the ...=> separator is executed before the function terminates with return or throw. The example above could also be written:

      files!scoreFile.Open(read) ...=> files!scoreFile.Close()
return files!scoreFile.ReadInt()

Predefined types and objects

C types and compatibility

To make things easy for the programmer, a number of C types are implemented in dodo. That includes int, char, double, enum, struct... Characters are Unicode (which can include more than one code point) and there is a byte type like in Java.

Floating point literals cannot end with a dot.

There is a bool type for boolean values and a flag type for bit values. Dodo has references but no pointers.

Dodo specific classes

In dodo class is really a prototype instance of class Object. The objects have an attribute self that designate themselves. The parent class is called Super.

The prototype for all dodo types is dodo. The type dodo has a class function Super that returns its parent class and a transition function const that returns a copy of the instance that cannot change.

Text Strings

Text strings in dodo are of generic type String. The generic type String is Indexed, so the elements of the string can be accessed using an index in brackets and the number of elements is returned by the count attribute.

Elements of the string are 32-bit numbers. Their range of possible values depends on the string encoding. The string encoding, in turn, depends on the encoding of the source file that contains the string literal.

The encoding for a string literal can otherwise be specified with a specifier before the opening quote. These are the mappings:

NotationEncodingElt bit sizeComments
o""UTF-88
x""UTF-16LE16little endian, eg. Intel x86
X""UTF-16BE16big endian, eg. Sparc
L""Latin-18
@""ASCII7

Operations on two strings with different encodings are not allowed, except if one of them is an unmarked string literal. A string can be re-encoded with a different encoding but some characters may be lost in the conversion.

The generic String type has a conversion to char[]. This conversion splits Unicode ligatures into two characters. The length of the string in term of chars is returned by the len attribute.

String literals contain no escaped characters. Use the Java notation for escaped characters out of strings. For example:

      "Please say " + \" + "Hello!" + \"  #Please say "Hello!"

In a multi-line string each line starts with a quote and ends without a quote until the end of the string. A triple quote can be used instead at the end of the line to allow for comments. Example:

      String notice = "      Dodo, The Programming Language      
" "( center(\u00A9 + " " + author, 40) )" """ #continued below
"
" It makes you a good programmer easily!
" It contributes to world peace!
" It grinds coffee!
"
" Get dodo now. "

Arrays, maps and sets

In contrast with C and Java, dodo arrays start at index 1. Multidimensional arrays have a list of indices, although the Java notation is also supported. For example:

      int[10, 3] matrix # Standard dodo notation
int[3][10] lines # Java notation - by convention, the dimensions are in the
# order they are used

The first declares a 2-dimensional array (matrix) with 30 elements arranged in three lines. The second declares an array with three elements each of type int[10].

Because both dimensions are specified the array has a conversion to the standard dodo notation. If a dimension is left blank there is no valid conversion to the dodo notation.

The dodo notation has a conversion to a flat array and to the Java notation. This is a summary of possible conversions:

T[x, y]T[x * y]
T[x, y]T[y][x]
T[y][x]T[x, y]
T[x][](no conversion)

Access to elements of the array must use the same notation as its declaration. List literals are written as a list of comma-separated values inside square brackets.

Dodo also has maps which are like arrays, except they are indexed with objects responding to qualifier Keyed instead of integers. Strings and enums can be used as indexes for maps. For example:

      Colour[String name] colour =
["red": red, "yellow": yellow, "green": green, "black": black]

This also declares colour.name, a variable which yields each key of the map in turn in the local scope. The type of the key is String with the additional rule that it has to be part of the key set of the map. The types derived from the colour prototype also have the same key type, so their key set is a subset of the colour key set. To iterate over all the keys of the map you can do:

      loop foreach (colourName in colour.name):
console!out.Puts(colourName)
.

Finally a set contains unique values in no particular order. The elements of the set should be Keyed. For example:

      Game[:] childGames = [:hideAndSeek, :playTag, :jumpRope, :running]

The set Type[:] has a conversion to the map Type[Type k]. This is useful to define a set of enum values a variable can take. Example of use:

      def[enum day] week =
[:monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday]
week.day aDay #can be any day of the week

A set also has a member attribute which can be used in the same way.

Arrays, maps and sets can be in either fullAccess or readOnly state. When one of them is passed by value it is always in readOnly state.

When the state is fullAccess, elements of the object can be used in full access, changed, added and removed. You can use const to set the state to readOnly. When the state is readOnly, elements of the object can be used in read only mode. They cannot be changed, added or removed. There is no transition from readOnly to fullAccess.

Intervals

Dodo has a special syntax for intervals. Intervals can be used with types that respond to qualifier Ordered.

The first example of interval is the form:

      n+

That notation means "n or more". Used as a list index, it allows to select all elements of the list from index n onwards. An equivalent is:

      n...

You can use the ellipsis with an upper bound too. Consider the notations:

      ...n
m...n

They represent respectively "up to n" and "from m to n". Used as a list index, the first allows to select all elements up to n. The second selects all elements which index is part of the range.

To represent all values that are not in an interval, use the ! operator. Intervals can be combined with the || operator, for example:

      1 || 2 || 3 || 9...15 || 99+

As lists are indexed starting from 1 by default, the size of the list is also the index of the last element. This notation can be used in a list index expression to give the list size:

      $$

For list a, that is the equivalent of a.count. An index expression can also be a list of indexes in the standard list notation. For example:

      a[[31, 2, $$, 9]]  #same as [a[31], a[2], a[$$], a[9]]

Predefined qualifiers

A notable predefined qualifier for dodo objects is Polymorphic. This qualifier alters the structure of the object in such a way that it supports polymorphism, which means that a same class can exist in a variety of forms which are its subclasses.

For example, we could say House, school and eiffelTower are all subclasses of Building. A function that takes a Building as parameter and calls draw to make a drawing of it will see all sorts of shape and colour, thanks to polymorphism, not just a bunch of generic gray Buildings.

All objects that inherit from Object are polymorphic. Methods and C types are not polymorphic. In dodo Polymorphic also implies Tagged, which means the object keeps track of its state in an attribute.

Other qualifiers provided as standard are Abstract, Arithmetic, Logical, Indexed, Keyed, Ordered, Iterable, Augmentable, Enumerable, Countable, Shared, Mutable, Editable and Versioned.

QualifierOperationsType attributes
Arithmetic- * / ** null magnitudezero unit
Augmentable+
Logical|| && ! ^^
Indexed[] count contains indexOf
Keyedidentifier conflictsuniqueId
Ordered< <= > >= min maxlowerBound upperBound
Iterable& << >> >>> value filter
apply all each zip
constantSpace
Enumerable++ -- %
Countable<: <:= :> :>= <> voidnothing universe
MutableValidate
EditablePut Delete

An Abstract class cannot be instantiated. However its subclasses can be instantiated. The Abstract qualifier is often used to make a pattern, that is a class which subclasses are interchangeable.

Earlier I promised to talk about attributes that can change after they are set. If the object responds to the Mutable qualifier, its attributes are not frozen and are allowed to change by invoking a method on them. Like arrays, Mutable objects have a fullAccess and a readOnly state.

The Editable qualifier allows to assign new values to the object attributes. The Editable qualifier implies Mutable. An object qualified Editable can be modified anywhere in a method or constructor. Existing attributes can be edited and new attributes can be added. That flexibility has its price, though. Editable objects are not much parallel-friendly.

An alternative to the Editable qualifier is Versioned. Attributes of a Versioned object can be modified (though new attributes cannot be added), and a Versioned object does not impede parallelism. The Versioned qualifier allows backtracking. However the programmer may end up with separate, incompatible versions of the same object. If desired, a handler can be defined to reconcile all the versions of the object.

If the type is Tagged then it stores the state in its instances. The Polymorphic qualifier implies Tagged. A transition function can be used to go from one state to the other, updating the stored state.

Parallelism and concurrency

Associative function

Some functions are specially friendly to parallel scheduling. Dodo functions can be declared associative. An example of associative function is addition:

      add(add(3, 4), 1) = add(3, add(4, 1)) = 8

Note that declaring associative functions helps concurrency only when they take longer to evaluate. This is how you can declare an associative function in dodo:

      associative functionName(Type parameter1, parameter2) block | =value
associative functionName(Type, Type)

It is allowed to call an associative function with an arbitrary number of arguments. Dodo will decide how to group the arguments in pairs for optimal execution. For example:

      associative min(int a, b) = if (a < b) a else b
min(12, -3, 5, 0, 40)

Finally, an associative lambda expression (See Expressions) is written:

      fun (associative Type parameter1, parameter2) [-> continuation Type, ...] {expression}
Fun(associative -> Type, ...)

Messaging

In dodo, access to essential parts of the system is done through messages. This access is called a capability and is always granted by the main program. The only exception is that a capability itself can grant another capability for the same service.

Example of capability:

      def scoreFile = files.File("scores", read) #capability scoreFile
...
files!scoreFile.Open(read) #send message "scoreFile.Open"

Messages are like method calls, but they don't wait for the method to return and can be sent both in methods and in functions. A function that uses messages is said impure, because the same call can yield different results. That can have an adverse effect on some optimisations.

All functions that take a capability as parameter are potentially impure.

Sending a message returns a message object. If the sent message has a return value, it can be read using the conversion function of the returned message. However the execution of the program will be suspended until the return value is available. For example:

      message result = files!scoreFile.ReadInt()  #send message
int score = result #wait for the result and copy in score

If the processing of the message produces an exception, it is raised when the return value is read. The exception can also be retrieved using the event service.

Since messages are typically exchanged between different processes or threads of execution, dodo allows to decide what should happen in case the recipient fails or is not reachable. The handler can include code that is executed after a failure or a timeout.

      #retry up to 3 times then fail, timeout 3, 4 and 5 seconds
handler[scoreFile] (retries: 3, timeout: [3000,4000,5000], action: fail)

#no retry, continue after 1 second timeout
handler[scoreFile] (timeout: [1000])

Threads and shared variables

To spawn a new thread for calculations that should happen in parallel, use a fork block. Dodo builds a dependency graph to determine which threads to run at a given time.

The share service allows to share data between parallel threads of execution. That data can be modified using the share service. A shared type and a shared variable are declared with:

      def SharedType = new Share(this: Type)
Share(<this: Type>) sharedVar

Example of function using shared data:

      DirtyPlate lunch(!SharedFork leftFork, rightFork, Dish food)
{
share!leftFork.Pick() ...=> share!leftFork.Drop()
share!rightFork.Pick() ...=> share!rightFork.Drop()
return food.eat
}

Transactions

Some parts of code can be made into a transaction using a sync block. Normal shared variables used in a transaction cannot be written concurrently until the transaction finishes, ensuring that the transaction is atomic.

However, a shared versioned variable can be read or written concurrently during a transaction. The transaction uses a copy of the variable. When the transaction finishes, dodo updates the shared variable with the transaction copy.

The programmer can define a more refined behaviour at the end of the transaction using a handler on the shared variable. The parameters of the handler are:

timeout: [ms1,...]terminates the transaction if the variable cannot be read or written for ms1 milliseconds.
retries: nif the transaction terminates or fails, try it again until it runs n times
use: newest(default) always replace the shared variable with the transaction copy
use: firstif the version of the shared variable changed, discard the transaction copy
action: fail, use: newestif the shared value is changed and is based on a different version of the variable, the transaction fails
action: fail, use: firstif the version of the shared variable changed, the transaction fails
action: merge[, use: newest]attempt to merge all the changes; in case of conflict, change the shared variable according to the transaction copy
action: merge, use: firstattempt to merge all the changes; in case of conflict, discard the change in the transaction copy
action: merge, store: $nameattempt to merge all the changes; store the list of conflicts in a variable with the specified name

Sample use of merge:

      handler[account] (action: merge, store: $conflicts)
{
loop foreach (conflict in conflicts):
test
{
conflict ~ ?_(field: balance, actual: $b0, next: $b1):
#New balance is current + next - initial
.balance = balance + b1 - b0
.
}
.
}

sync:
.account.balance += deposit
.

Note that merge can result in a variable with incoherent state if used without care. Also outside of the class, conflicts involving private attributes will not be stored (the newest value is used). Ideally the conflict handler should not make heavy computations or access resources. That would hurt concurrency.