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

  • Readability: it should not be difficult for most of the public to understand a dodo program. I want to use imperative style with objects, which should help in that sense.
  • Safety: programming errors should be scarce, they should be found early and be easy to trace. For that reason anything that introduces indeterminism or side-effects should be avoided whenever possible.
  • Concurrency: programs should make good use of modern hardware and operating system concurrent capabilities. To that end parallelism should be a salient feature of the language.
  • Structure of programs

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

  • Header file: contains the public interface of objects. It can be used as reference and should be well documented for better reuse. The file extension is .dodoh or .doh.
  • Source file: contains the objects code and details of implementation. The file extension is .dodoo or .doo.
  • Library file: contains the list of files that make a library along with contextual information. The file extension is .dodoa or .doa.
  • Combined file: contains any combination of the above structured using the XML syntax. The file extension is .dodo or .do. The combined file could look like:
  • <?xml version="1.0" encoding="ISO-8859-1"?>

    <header name="modulename">

       [...header code...]

    </header>

    <source name="modulename">

       [...source code...]

    </source>

    <library name="libraryname">

       [...library...]

    </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:

    int x = 36

    String format(Pattern, Date)

    Date today, tomorrow

    Several statements can be written on the same line. The separator is semicolon. A single statement can be broken in several lines in some cases, then a semicolon may be required to terminate it.

    The program can contain comments using the XML syntax or single-line comments introduced with the hash character:

    # Single-line comment

    <!-- XML style comment -->

    A block is delimited with C-style curly braces or with a colon and a period. The ending period should always be preceded 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.

    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

    For function parameters with a default value, use a question mark instead:

    Drawing draw(? support = Canvas())  #support of type Canvas

    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

       <!-- 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?)

       <!-- Make a drawing of the house

          #@ Canvas support: 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. In contrast to other programming languages it is not necessary to call a parent constructor in the constructor.

    The example above shows a function draw that has a parameter of type Canvas. A function has a different return value depending 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 expressed as a function. 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 [is Qualifier, ...]

    |

    def ClassName = new Superclass[(attribute value, ...)] [is Qualifier, ...]

    {

       [...links...]

       [...attributes...]

       [...constructors...]

       [...functions...]

       [...methods...]

    [<state name="stateName" [in="parentState"]>

       [...transitions...]

       [...attributes...]

       [...functions...]

       [...methods...]

    </state>

    ...]

    }

    There are several things that need explanation here. You noticed that the example used class as prototype; this is a generic object that can be used to build a class. All objects of the new class will be based on the prototype. If you want your class to inherit properties and operations from another class, making effectively a more specialised version of it, specify it as superclass.

    A qualifier can be specified to add properties 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. 

    Methods are similar to functions. But they are not required to return always the same value when they are provided the same 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. Attributes, functions and methods declared outside a state block are always available. However those declared inside a state block are only available in that state. 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 idea 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 sketch would be available for a house in the state "hasFeatures" or "hasColours", while draw 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 tail = null

    }

    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 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 = new House(windowCount 6, wallCol green)()

    test match(building):

       &/House(wallCol $c) {return "The " + c + " house"}

       &/EiffelTower {return "The Eiffel Tower"}

       &/School(name $n) {return "The " + n + " school"}

    .

    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.

    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.

    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.

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

    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 name="open">

       Transition((to closed)) close

       method Play: Play(.Child[], Area?)

          ->(Game) played.

    </state>

    <state name="closed">

       Transition((to open)) open

    </state>

    }

    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...]

       [...functions...]

       [...methods...]

    [<state name="stateName" [in="parentState"]>

       [...transitions...]

       [...attributes...]

       [...functions...]

       [...methods...]

    </state>

    ...]

    }]

    It is possible to declare a class based on a prototype, just use that prototype as the superclass.

    Qualifier definition

    A qualifier helps to add specific properties 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 wraps the draw function in House with code that draws the inhabitants in addition to the house:

    qualifier Inhabited

    {

       Link((to People[])) inhabitants

    <wrap>

       draw($support):

          Drawing result = draw(support)

          loop foreach(inhabitant in inhabitants)

          {

             .result = inhabitant.draw(result) #Drawing can act as Canvas

          }

          return result

       .

    </wrap>

    }

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

    qualifier Notifying

    {

       console.Output Notifying.out

    <wrap selector="sel">

       $function($argument*):

          console!out.Puts("Calling " + function.name + " * " + argument.count)

          try

          {

             return sel.eval

          }

          console!out.Puts("Finished " + function.name)

       .

    </wrap>

    }

    Dodo allows to add qualifiers to a variable or a type using qualify. 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 qualify is:

    qualify name is Qualifier, ...

    |

    qualify Type is Qualifier, ...

    Patterns can be used for the type. All types that match the pattern will be added the qualifier. For example:

    qualify files.$_ is new Notifying(out con) #add logging to all types in files

    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 declare additional functions with the same name, but which replace the type of the parameter with one of its subclasses inside dispatch(). So if the function is:

    Type function(ParamType, ...)

    Another declaration that adds multiple dispatch would be:

    Type function(dispatch(Subtype), ...)

    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 support):

       #Draw with pencils

    .

    Drawing draw(dispatch(Blackboard) board):

       #Draw using chalk

    .

    Note that multiple dispatch is particularly useful when the operation varies depending on more than one parameter type. The special type Self can be used together with dispatch in subclasses.

    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

    {

       ->(String) story:

          String description = "A house with %i windows, a %s roof and %s walls"

          return String.sprintf(description, 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[filter self != child], area)

          .

          .played = (map children.game).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.

    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)

    .total = 15

    if (x > 4.0) x else f(x)

    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(...Types...) [=value], ...; expression}

    fun(...ParamTypes... -> (...Types...), ...)

    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).

    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 -> return int

       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 | ...

    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 that are instance attributes also have a parameter of the instance type named self in first position.

    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 values corresponding to the first match in the list is returned. For example:

    selection ~ match [/^([0-9]*)/, /date: ([0-9]{2})\/([0-9]{4})/]

       -> (n)   getRecord(id n)

       | (m, y) Record.newWithDate(Date.parse(y + m, "YYYYMM"))

    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 map and the filter constructs are designed for this purpose. The general form of map and filter is:

    map list.attribute[(argument, ...)]

    |

    function(map list, ...)

    list[filter condition]

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

    map houses[filter 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 fold/seed construct should be used for that. The general form of fold is:

    fold list.function(seed [initialValue], ...)

    |

    function(fold list, seed [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 (with the exclusion of an eventual seed). Instead the seed is returned. You should chose your seed carefully taking into account the case of an empty list.

    To illustrate the use of fold, let us rewrite the wrapping 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:

    draw($support):

       return fold inhabitants.draw(seed draw(support))

    .

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

    unfold {parameter = value; condition; next [in expression]}

    The default condition is true, and the default expression is the parameter. The terms of the unfold are evaluated as needed (lazily). For example to calculate the square numbers from 1 to 5:

    unfold: x = 1; x <= 5; ++x in x * x. #gives [1, 4, 9, 16, 25]

    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 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. The loop finishes when no condition evaluates true or no match is found for the expression. The default block is executed once only if there is no other match on first iteration.

    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

    }

    Another way to get the results of a yielding function is to give it a label and combine it with resume. This combination provides a convenient way for two functions to communicate. When a yielding function does return the caller regains temporarily control of the execution, until the caller does resume to return the hand. For example:

    Notes collectNames(Child[] children)

    {

       Notes notepad

       String name = (question(children) -> $$ask)

       .notepad = notepad.write("First child: " + name)

       .notepad = notepad.write("Second child: " + resume $$ask)

       .notepad = notepad.write("Third child: " + resume $$ask)

       loop foreach (name in resume $$ask)

       {

          String nth = ++notepad.lines.count + "th"

          .notepad = notepad.write(nth + " child: " + name)

       }

       return notepad

    <catch event="e">

       test (e ~ &/EndOfYield):

          return notepad  #there are less than 3 children

       .

    </catch>

    }

    With return the yielding function saves its state and allows the caller to continue. The resume instruction causes the yielding function to resume at the place it left.

    When the yielding function terminates the  EndOfYield event is raised.

    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. There can be one event handling block per function. The syntax of an event handling block is:

    <catch event="variable">

       [...instructions...]

    </catch>

    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:

       int bestGuess = (map $$1 children.guess).average

       return "The children guessed: " + bestGuess

    <catch event="oops">

       resume $$1 5  #resume with 5 if the child did a mistake

    </catch>

    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(fRead)

    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 UNDO.

    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 ==> separator, 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(fRead) ==> 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 and there is a byte type like in Java.

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

    Dodo specific classes

    The prototype for all dodo types is dodo. The type dodo has a function parent() that reurns its parent and a function const() that returns a copy of itself that cannot change.

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

    Dodo has arrays, however the first element of the array is at index 1 unlike C. Also the following declarations are different:

    int[10][3] lines

    int[10, 3] matrix

    The first one declares an array with 3 elements, each of type int[10].

    The second declares a 2-dimension array (matrix) with 30 elements arranged in 3 lines. The following conversions are allowed:

    int[10][3] n = matrix

    int[30] o = matrix

    On the other hand lines cannot be converted because it is a 1-dimension array.

    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[for String] colour = ["red" red, "yellow" yellow, "green" green, "blue" blue, "black" black]

    Arrays and maps can be in either fullAccess or readOnly state. When the state is fullAccess, elements of the array or map 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 array or map can be used in read only mode. They cannot be changed, added or removed. The state of the array or map cannot be changed back to fullAccess.

    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.

    Other qualifiers provided as standard are Abstract, Arithmetic, Logic, Indexed, Ordered, Keyed, Clonable, Shared, Mutable, Editable and Versioned.

    An Abstract class cannot be instanciated. However its subclasses can be instanciated. 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.

    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 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", fRead) #capability scoreFile

    ...

    files!scoreFile.Open(fRead) #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. If the return value is not used, when the next exception is raised the exception gets attached to it. If that never happens, the exception is ignored.

    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 for="scoreFile" retries="3" timeout="3000,4000,5000" action="fail" />

    #no retry, continue after 1 second timeout

    <handler for="scoreFile" timeout="1000" />

    The share service

    The share service allows to share data between parallel threads of execution. That data can be modified using messaging. Dodo ensures that the data stays coherent by building a dependency graph.

    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. Normal shared variables used in a transaction cannot be read or 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 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="n"

    if the transaction terminates or fails, try it again until it runs n times

    use="new"

    (default) always replace the shared variable with the transaction copy

    use="old"

    if the version of the shared variable changed, discard the transaction copy

    action="fail" use="new"

    if the transaction copy is based on an older version of the shared variable, the transaction fails

    action="fail" use="old"

    if the version of the shared variable changed, the transaction fails

    action="merge" [use="new"]

    attempt to merge all the changes; in case of conflict, change the shared variable according to the transaction copy

    action="merge" use="old"

    attempt to merge all the changes; in case of conflict, discard the change in the transaction copy

    action="merge" store="name"

    attempt to merge all the changes; store the list of conflicts in a variable with the specified name


    Sample use of merge:

    <handler for="account" action="merge" store="conflicts">

       loop foreach(conflict in conflicts):

          test match(conflict)

          {

             balance($b0, $b1):

                #New balance is previous + current - initial

                .account.balance += b1 - b0

             .

          }

       .

    </handler>

       transaction:

          .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 reported (the newest value is used). Ideally the conflict handler should not make heavy computations or access resources. That would hurt concurrency.