Wednesday, August 1, 2012

A pointer-free balanced "AA_Tree" in ParaSail

Talking about pointer-free data structures without an example is difficult. Here is an example from the "standard library" of ParaSail. It is a balanced "AA" tree. First we have the interface to the AA_Tree module, then the class that implements it, and finally a simple test function "Test_AA_Tree."

Note that this is just part of the ParaSail standard library, which is included in the downloadable prototype compiler and virtual machine. See a separate blog entry for the most recent downloadable version. You can run the test program after installing the ParaSail release by the command "pslc aaa.psi -command Test_AA_Tree 2 9 4". See the appendix in the ParaSail reference manual for more information on running the ParaSail compiler and virtual machine.

// ParaSail Prototype Standard Library

// Copyright (C) 2011-2012, S. Tucker Taft, Lexington MA, USA
// To be used only for Personal, Academic, or Evaluation Purposes;
// Not for Commercial Production Use.
// Report errors at http://groups.google.com/group/parasail-programming-language

interface AA_Tree<Element is Comparable<>> is

    // This module implements a balanced "AA" tree, originally
    // described by Arne Andersson in the "Proceedings of the Workshop
    // on Algorithms and Data Structures," pp 60-71, Springer Verlag, 1993.
    // The following algorithm and descriptions were taken from the
    // WikiPedia article on AA_Tree: 
    //       http://en.wikipedia.org/wiki/AA_tree
    // Note that various additional checks for a null tree have been added.

    // Only two operations are needed for maintaining balance in an AA tree.
    // These operations are called skew and split. Skew is a right rotation
    // when an insertion or deletion creates a left horizontal link. Split
    // is a conditional left rotation when an insertion or deletion creates two
    // horizontal right links, which once again corresponds to two
    // consecutive red links in red-black trees.

    op "[]"() -> optional AA_Tree;
        // Create an empty tree

    func Insert(var T : optional AA_Tree; X : Element);
        // input: X, the value to be inserted, and 
        // T, the root of the tree to insert it into.
        // output: A balanced T' including X.

    func Delete(var T : optional AA_Tree; X : Element);
        // input: X, the value to delete, and T, 
        // the root of the tree from which it should be deleted.
        // output: T', balanced, without the value X.

    op "in"(X : Element; T : optional AA_Tree) -> Boolean;

    func Overlapping(T : optional AA_Tree; X : Element) -> optional Element;
        // input: X, the value to find, and T, 
        // the root of the tree to be searched.
        // output: the element equal to or "unordered" relative to X.

    op "|="(var T : optional AA_Tree; X : Element) is Insert;

    op "<|="(var T : optional AA_Tree; var X : optional Element);
        // Move X into AA_Tree, leaving X null.

    func First(T : optional AA_Tree) -> optional Element;
      // Return first (smallest) element in tree

    func Last(T : optional AA_Tree) -> optional Element;
      // Return last (greatest) element in tree

    func Remove_First(var T : optional AA_Tree) -> optional Element;
      // Remove first (smallest) element in tree

    func Remove_Last(var T : optional AA_Tree) -> optional Element;
      // Remove last (greatest) element in tree

    func Remove_Any(var T : optional AA_Tree) -> optional Element;
      // Remove some element from tree

    func Count(T : optional AA_Tree) -> Univ_Integer;
      // Return a count of the nodes in the tree

    func Is_Empty(T : optional AA_Tree) -> Boolean;
      // Return True if the tree is empty

end interface AA_Tree;

class AA_Tree is
    var Value : Element;
    var Level : Univ_Integer := 0;
    var Left : optional AA_Tree;
    var Right : optional AA_Tree;

    func Node(var Value : optional Element; Level : Univ_Integer; 
      Left, Right : optional AA_Tree) -> AA_Tree is
        // Create a new tree; move Value into it.
        return (Value <== Value, Level => Level, Left => Left, Right => Right);
    end func Node;

    func Is_Leaf(T : optional AA_Tree) -> Boolean is
        return T not null and then
          T.Left is null and then T.Right is null;
    end func Is_Leaf;

    func Leftmost(ref T : optional AA_Tree) -> ref optional AA_Tree is
        for L => T loop
            if L not null and then L.Left not null then
                // Continue with Left until we reach null
                continue loop with L.Left;
            else
                // Found left-most
                return L;
            end if;
        end loop;
    end func Leftmost;

    func Successor(T : optional AA_Tree) -> optional Element is
        // Return element in tree greater than but closest to T.Value
        if T.Right not null then
            const Succ := Leftmost(T.Right);
            {Succ not null};
            return Succ.Value;
        else
            return null;
        end if;
    end func Successor;

    func Rightmost(ref T : optional AA_Tree) -> ref optional AA_Tree is
        for R => T loop
            if R not null and then R.Right not null then
                // Keep following down Right side
                continue loop with R.Right;
            else
                // Found right-most
                return R;
            end if;
        end loop;
    end func Rightmost;

    func Predecessor(T : optional AA_Tree) -> optional Element is
        // Return element in tree less than but closest to T.Value
        if T.Left not null then
            return Rightmost(T.Left).Value;
        else
            return null;
        end if;
    end func Predecessor;

    func Skew(var T : optional AA_Tree) is
      // input: T, a node representing an AA tree that needs to be rebalanced.
      // output: T' Another node representing the rebalanced AA tree.

        if T not null and then
          T.Left not null and then
          T.Left.Level == T.Level then
            // The current T.Left becomes new root

            // Exchange value of T.Left with root
            T.Value <=> T.Left.Value;
           
            // Move old root and T.Left.Right over to right side of tree
            T.Left.Right <=> T.Right;
            T.Left.Left <=> T.Right;
            T.Left <=> T.Right;
        end if;
    end func Skew;

    func Split(var T : optional AA_Tree) is
        // input: T, a node representing an AA tree that needs to be rebalanced.
        // output: T' Another node representing the rebalanced AA tree.

        if T not null and then
          T.Right not null and then
          T.Right.Right not null and then
          T.Level == T.Right.Right.Level then
            // T.Right becomes the new root
            // Exchange value and level between root and T.Right
            T.Value <=> T.Right.Value;
            T.Level <=> T.Right.Level;

            // Move old root and T.Right.Left to left side of tree
            T.Left <=> T.Right.Right;
            T.Right.Left <=> T.Right.Right;
            T.Left <=> T.Right;

            // Increment level
            T.Level += 1;
        end if;
    end func Split;

    func Decrease_Level(var T : optional AA_Tree) is
        // input: T, a tree for which we want to remove links that skip levels.
        // output: T with its level decreased.

        if T is null then
            return;
        end if;
           
        var Should_Be : Univ_Integer := 1;

        if T.Left not null then
            Should_Be := T.Left.Level + 1;
        end if;

        if T.Right not null then
            Should_Be := Min(Should_Be, T.Right.Level + 1);
        end if;
            
        if Should_Be < T.Level then
            T.Level := Should_Be;
            if T.Right not null and then
              Should_Be < T.Right.Level then
                T.Right.Level := Should_Be;
            end if;
        end if;
    end func Decrease_Level;

  exports

    op "[]"() -> optional AA_Tree is
        // Create an empty tree
        return null;
    end op "[]";

    // Insertion begins with the normal binary tree search and insertion
    // procedure. Then, as the call stack unwinds (assuming a recursive
    // implementation of the search), it's easy to check the validity of the
    // tree and perform any rotations as necessary. If a horizontal left link
    // arises, a skew will be performed, and if two horizontal right links
    // arise, a split will be performed, possibly incrementing the level of the
    // new root node of the current subtree. Note, in the code as given above,
    // the increment of T.Level. This makes it necessary to continue checking
    // the validity of the tree as the modifications bubble up from the leaves.
    
    op "<|="(var T : optional AA_Tree; var X : optional Element) is
        // Move X into AA_Tree, leaving X null.
        // input: X, the value to be inserted, and 
        // T, the root of the tree to insert it into.
        // output: A balanced T' including X.

        // Do the normal binary tree insertion procedure. 
        // Set the result of the recursive call to the correct 
        // child in case a new node was created or the
        // root of the subtree changes.

        if T is null then
            // Create a new leaf node with X.
            T := Node(X, 1, null, null);
            return;
        end if;

        case X =? T.Value of
          [#less] =>
            T.Left <|= X;
          [#greater] =>
            T.Right <|= X;
          [#equal | #unordered] =>
            // Note that the case of X == T.Value is unspecified. 
            // As given, an insert will have no effect. 
            // The implementor may desire different behavior.
            X := null;
            return;
        end case;

        // Perform skew and then split. 
        // The conditionals that determine whether or
        // not a rotation will occur or not are inside 
        // of the procedures, as given above.

        Skew(T);
        Split(T);
    end op "<|=";

    func Insert(var T : optional AA_Tree; X : Element) is
        // Just pass the buck to the "<|=" operation
        var X_Copy for T := X;
        T <|= X_Copy;
    end func Insert;

    // As in most balanced binary trees, the deletion of an internal node can
    // be turned into the deletion of a leaf node by swapping the internal node
    // with either its closest predecessor or successor, depending on which are
    // in the tree or on the implementor's whims. Retrieving a predecessor is
    // simply a matter of following one left link and then all of the remaining
    // right links. Similarly, the successor can be found by going right once
    // and left until a null pointer is found. Because of the AA property of
    // all nodes of level greater than one having two children, the successor
    // or predecessor node will be in level 1, making their removal trivial.
    // 
    // To re-balance a tree, there are a few approaches. The one described by
    // Andersson in his original paper is the simplest, and it is described
    // here, although actual implementations may opt for a more optimized
    // approach. After a removal, the first step to maintaining tree validity
    // is to lower the level of any nodes whose children are two levels below
    // them, or who are missing children. Then, the entire level must be skewed
    // and split. This approach was favored, because when laid down
    // conceptually, it has three easily understood separate steps:
    // 
    //     Decrease the level, if appropriate.
    //     Skew the level.
    //     Split the level.
    // 
    // However, we have to skew and split the entire level this time instead of
    // just a node, complicating our code.

    func Delete(var T : optional AA_Tree; X : Element) is
        // input: X, the value to delete, and T, 
        // the root of the tree from which it should be deleted.
        // output: T', balanced, without the value X.

        if T is null then
            // Not in tree -- should we complain?
            return;
        end if;

        case X =? T.Value of
          [#less] =>
            Delete(T.Left, X);
          [#greater] =>
            Delete(T.Right, X);
          [#equal] =>
            // If we're a leaf, easy, otherwise reduce to leaf case. 
            if Is_Leaf(T) then
                T := null;
            elsif T.Left is null then
                // Get successor value and delete it from right tree,
                // and set root to have that value
                const Succ := Successor(T);
                Delete(T.Right, Succ);
                T.Value := Succ;
            else
                // Get predecessor value and delete it from left tree,
                // and set root to have that value
                const Pred := Predecessor(T);
                Delete(T.Left, Pred);
                T.Value := Pred;
            end if;
          [#unordered] =>
            // Not in tree; should we complain?
            return;  
        end case;

        // Rebalance the tree. Decrease the level of all nodes in this level if
        // necessary, and then skew and split all nodes in the new level.

        if T is null then
            return;
        end if;

        Decrease_Level(T);
        Skew(T);
        Skew(T.Right);
        if T.Right not null then
            Skew(T.Right.Right);
        end if;
        Split(T);
        Split(T.Right);
    end func Delete;

    op "in"(X : Element; T : optional AA_Tree) -> Result : Boolean is
        for P => T while P not null loop
            case X =? P.Value of
              [#less] =>
                continue loop with P.Left;
              [#greater] =>
                continue loop with P.Right;
              [#equal] =>
                return #true;
              [#unordered] =>
                return #false;
            end case;
        end loop;
        return #false;  // Not found
    end op "in";

    func First(T : optional AA_Tree) -> optional Element is
      // Return first (smallest) element in tree
        if T is null then
            return null;
        else 
            return Leftmost(T).Value;
        end if;
    end func First;

    func Last(T : optional AA_Tree) -> optional Element is
      // Return last (greatest) element in tree
        if T is null then
            return null;
        else
            return Rightmost(T).Value;
        end if;
    end func Last;


    func Remove_First(var T : optional AA_Tree) -> Result : optional Element is
      // Remove first (smallest) element in tree
        Result := First(T);
        if Result not null then
            Delete(T, Result);
        end if;
    end func Remove_First;

    func Remove_Last(var T : optional AA_Tree) -> Result : optional Element is
      // Remove last (greatest) element in tree
        Result := Last(T);
        if Result not null then
            Delete(T, Result);
        end if;
    end func Remove_Last;

    func Remove_Any(var T : optional AA_Tree) -> Result : optional Element is
      // Remove some element from tree
        if T is null then
            return null;
        end if;
        Result := T.Value;
        if Result not null then
            Delete(T, Result);
        end if;
    end func Remove_Any;

    func Is_Empty(T : optional AA_Tree) -> Boolean is
      // Return True if the tree is empty
        return T is null;
    end func Is_Empty;

    func Count(T : optional AA_Tree) -> Univ_Integer is
      // Return a count of the nodes in the tree
        if T is null then
            return 0;
        else
            return Count(T.Left) + Count(T.Right) + 1;
        end if;
    end func Count;

    func Overlapping(T : optional AA_Tree; X : Element) -> optional Element is
        // input: X, the value to find, and T, 
        // the root of the tree to be searched.
        // output: the element equal to or "unordered" relative to X.
        if T is null or else T.Value is null then
            return null;
        else
            case X =? T.Value of
              [#less] =>
                return Overlapping(T.Left, X);
              [#greater] =>
                return Overlapping(T.Right, X);
              [#equal | #unordered] =>
                // Close enough
                return T.Value;
            end case;
        end if;
    end func Overlapping;

end class AA_Tree;

func Test_AA_Tree(A : Univ_Integer; B : Univ_Integer; C : Univ_Integer) is
    type Univ_Tree is AA_Tree<Univ_Integer>;
    var T : Univ_Tree := [];
    var X : Univ_Integer := A;

    Insert(T, A);
    Println("Count = " | Count(T) | " after insert of " | A);
    Insert(T, B);
    Println("Count = " | Count(T) | " after insert of " | B);
    Insert(T, C);
    Println("Count = " | Count(T) | " after insert of " | C);

    Insert(T, A);
    Println("Count = " | Count(T) | " after another insert of " | A);

    Println(A | " in T = " | (A in T));
    Println(B | " in T = " | (B in T));
    Println(C | " in T = " | (C in T));
    Println("7 in T = " | (7 in T));

    for E := Remove_First(T) then Remove_First(T) while E not null loop
        Println("Remove_First = " | E);
    end loop;

    Println("Count after loop : " | Count(T));

    for I in 1..10 forward loop
        Insert(T, I);
        Println("Count = " | Count(T) | " after insert of " | I);
    end loop;

    for L := Remove_Last(T) then Remove_Last(T) while L not null loop
        Println("Remove_Last = " | L);
    end loop;

    Println("Count after loop : " | Count(T));

    for J in 1..10 reverse loop
        Insert(T, J);
        Println("Count = " | Count(T) | " after insert of " | J);
    end loop;

    Println("Count after loop : " | Count(T));

    Println("Overlapping(T, 5) = " | Overlapping(T, 5));

    for Z := Remove_Any(T) then Remove_Any(T) while Z not null loop
        Println("Remove_Any = " | Z);
    end loop;

    Println("Count after loop : " | Count(T));

    for K in 1..10 loop
        Insert(T, K);
        Println("Count = " | Count(T) | " after insert of " | K);
    end loop;

    for F := Remove_First(T) then Remove_First(T) while F not null loop
        Println("Remove_First = " | F);
    end loop;

    Println("Count after loop : " | Count(T));

end func Test_AA_Tree;

A Pointer-Free Path to Object-Oriented Parallel Programming

The following is a first draft of a submission for the forthcoming "Foundations of Object-Oriented Languages" (FOOL 2012) workshop accompanying this year's SPLASH/OOPSLA conference in Tuscon.  Any comments would be welcome.

-----------

ParaSail: Pointer-Free Path to Object-Oriented Parallel Programming

Pointers are ubiquitous in modern object-oriented programming languages, and many data structures such as trees, lists, graphs, hash tables, etc. depend on them heavily.  Unfortunately, pointers can add significant complexity to programming.  Pointers can make storage management more complex, pointers can make assignment and equality semantics more complex, pointers can increase the ways two different names (access paths) can designate the same object, pointers can make program analysis and proof more complex, and pointers can make it harder to "divide and conquer" a data structure for parallel processing.

Is there an alternative to using pointers?  ParaSail, a new parallel object-oriented programming language, adopts a different paradigm for defining data structures.  Rather than using pointers, ParaSail supports flexible data structuring using "expandable" (and shrinkable) objects, along with generalized indexing.  By eliminating pointers, ParaSail significantly reduces the complexity for the programmer, while also allowing ParaSail to provide pervasive, safe, object-oriented parallel programming.

Expandable and Optional Objects


An "expandable" object is one which can grow without using pointers, much as a house can grow through additions.  Where once there was a door to the back yard, a new screened-in porch can be added.  Where once there was only one floor, a new floor can be added.  The basic mechanism for expansion in ParaSail is that every type has one additional value, called "null."  A component can initially be null, and then be replaced by a non-null value, thereby expanding the enclosing object.  At some later point the enclosing object could shrink, by replacing a non-null component with null. 

Not every component of an object is allowed to be null.  The component must be declared as "optional" if it is allowed to take on a null value.  For example, a Tree structure might have a (non-optional) "Payload" component, and then two additional components, "Left" and "Right," which are each declared as "optional Tree."  Similarly, a stand-alone object may be declared to be of a type "T," or of a type "optional T."  Only if it is declared "optional" may it take on the null value.  The value of an object X declared as "optional" may be tested for nullness using "X is null" or "X not null."

Another example of a data structure using optional components would be a linked list, with each node having two components, one "Payload" component, and a "Tail" component of type "optional List."  There is also a built-in parameterized type, "Basic_Array<Component_Type>" which allows the Component_Type to be specified as "optional."  This allows the construction of a hash table with buckets represented as linked-lists, by declaring the "backbone" of the hash table as a "Basic_Array<optional List<Hash_Table_Item>>."  The components of the hash table would start out as null, but as items are added to the hash table, one or more of the component lists would begin to grow.

Assignment, Move, and Swap Operations

Because there are no pointers, the semantics of assignment in ParaSail are very straightforward, namely the entire right-hand-side object is copied and assigned into the left-hand side, replacing whatever prior value was there.  However, there are times when it is desirable to "move" a component from one object to another, or "swap" two components.  Because implementing these on top of an assignment that uses copying would be painful, in ParaSail, "move" and "swap" are separate operations.  The semantics of "move" is that the value of the left-hand-side is replaced with the value of the right-hand-side, and the right-hand-side ends up null.  For "swap," the values of the left- and right-hand-side are swapped.  Syntactically, ParaSail uses ":=" for (copying) assignment, "<==" for move, and "<=>" for swap.  The ParaSail compiler is smart enough to automatically use "move" semantics when the right-hand-side is the result of a computation, rather than an object or component that persists after the assignment.

As an example of where "move" might be used, if our hash table grows to the point that it would be wise to lengthen the backbone, we could create a new Basic_Array twice as large (for example), and then "move" each list node from the old array into the new array in an appropriate spot, rebuilding each linked list, and then finally "move" the new array into the original hash-table object, replacing the old array.  The "swap" operation is also useful in many contexts, for example when balancing a tree structure, or when sorting an array.

Cyclic Data Structures and Generalized Indexing

Expandable objects allow the construction of many kinds of data structures, but a general, possibly cyclic graph is not one of them.  For this, ParaSail provides generalized indexing.  The array-indexing syntax, "A[I]," is generalized in ParaSail to be usable with any container-like data structure, where A is the container and I is the key into that data structure.  A directed graph in ParaSail could be represented as an indexed set of Nodes, where the index is a unique node "Id" of some sort, with edges represented as Predecessor and Successor  indice sets that are components of each Node.   There is no harm in following a "dangling" unique node-id in ParaSail, as that is easy to detect because the node-id would be missing from the indexed set.  In a language with pointers, either a dangling pointer will cause a storage leak, because the target node cannot be reclaimed, or it will lead to a potentially destructive reference to reclaimed storage.

Region-Based Storage Management

Storage management without pointers is significantly simplified.  All of the objects declared in a given scope are associated with a storage "region," essentially a local heap.  As an object grows, all new storage for it is allocated out of this region.  As an object shrinks, the old storage can be immediately released back to this region.  When a scope is exited, the entire region is reclaimed.  There is no need for asynchronous garbage collection, as garbage never accumulates.

Every object identifies its region, and in addition, when a function is called, the region in which the result object should be allocated is passed as an implicit parameter.  This "target" region is determined by how the function result is used.  If it is a temporary, then it will be allocated out of a temporary region associated with the point of call.  If it is assigned into a longer-lived object, then the function will be directed to allocated the result object out of the region associated with this longer-lived object.  The net effect is that there is no copying at the call site upon function return, since the result object is already sitting in the correct region.

Note that pointers are likely still used "behind the scenes" in any implementation of ParaSail, but eliminating them from the surface syntax and semantics can eliminate essentially all of the complexity associated with pointers.  That is, a semantic model of expandable and shrinkable objects, operating under "value semantics," rather than a semantic model of nodes connected with pointers, operating under "reference semantics," provides a number of benefits, such as simpler storage management, simpler assignment semantics, easier analyzability, etc. 

Parallel and Distributed Programming

In addition to removing pointers, certain other simplifications are made in ParaSail to ease parallel and distributed programming.  In particular, there are no global variables; functions may only update objects passed to them as "var" (in-out) parameters.  Furthermore, as part of passing an object as a "var" parameter, it is effectively "handed off" to the receiving function, and no further reference may be made to the object, until the function completes.  In particular, no part of the "var" parameter may be passed to any other function, or even to this same function as a separate parameter.  This eliminates the possibility of aliasing between a "var" parameter and any other object visible to the function.  These two additional rules, coupled with the lack of pointers, means that all parameter evaluation may happen in parallel (e.g. in "F(G(X), H(Y))", G(X) and H(Y) may be evaluated in parallel), and function calls may easily cross address-space boundaries, since the objects are self-contained (with no incoming or outgoing references), and only one function at a time can update a given object.

Concurrent Objects

All of the above rules apply to objects that are not designed for concurrent access.  ParaSail also supports the construction of concurrent objects, which allow lock-free, locked, and queued simultaneous access.  These objects are *not* "handed off" as part of parameter passing, but instead provide operations which synchronize any attempts at concurrent access.  Three kinds of synchronization are supported.  "Lock-free" synchronization relies on low-level hardware-supported operations such as atomic load and store, and compare-and-swap.  "Locked" synchronization relies on automatic locking as part of calling a "locked" operation of a concurrent object, and automatic unlocking as part of returning from the operation.  Finally, "queued" synchronization is provided, which evaluates a "dequeue condition" upon call (under a lock), and only if the condition is satisfied is the call allowed to proceed, still under the lock.  A typical "dequeue condition" might be that a buffer is not full, or that a mailbox has at least one element in it.  If the dequeue condition is not satisfied, then the caller is added to a queue.  At the end of any operation on the concurrent object that might change the result of the dequeue condition for a queued caller, the dequeue condition is evaluated and if true, the operation requested by the queued caller is performed before the lock is released.  If there are multiple queued callers, then they are serviced in turn until there are none with satisfied dequeue conditions.

Pointer-Free Object-Oriented Parallel Programming


The pointer-free nature of ParaSail is not just an interesting quirk.  Rather, we believe it represents a significantly simpler way to build large object-oriented systems.  By itself it simplifies storage management, assignment semantics, and analyzability, and when combined with the elimination of global variables and parameter aliasing, it allows for the easy parallelization of all expression evaluation, and the easy distribution of computations across address spaces, without having to make the jump to a pure side-effect-free functional language.

Monday, July 30, 2012

Zero-based indexing for Vector and Univ_String

Whether array indices most "naturally" should start at zero or one remains a surprisingly unresolved question.  Algol, Pascal, Ada, Modula, Eiffel, etc. generally started array indexing at 1, or allowed the programmer to choose the bounds, while C with its model of array indexing being equivalent to pointer arithmetic, requires starting at zero since array indices are interpreted as offsets.

In ParaSail arrays are simply a special case of a container, and indexing is user-definable, so the index need not even be integers, and certainly there is no requirement that the first index be zero, or one, or any specific value.  On the other hand, there are some pre-provided modules such as Vector and Univ_String which allow indexing, and some decision has to be made about whether to start with zero or one.  Because of the Pascal/Ada/Modula heritage, the natural choice was to start with one.

However, there are algorithms where starting with zero is more natural, and there are certainly programmers who are more comfortable with starting indexing with zero, so we have recently added zero-based indexing versions of Vector and Univ_String, namely ZVector and ZString.  So using ParaSail's half-open intervals, one can loop over the contents of a ZVector (or a ZString) with:
    var ZV : ZVector<Univ_Integer> := [1, 3, 5, ...];
    var Sum := 0;

    for I in 0 ..< Length(ZV) loop
        Sum += ZV[I];
    end loop;

Monday, July 23, 2012

More electronic ink on ParaSail

Another nice article on ParaSail just came out, in the on-line journal EEJournal:

    http://www.eejournal.com/archives/articles/20120718-language/

This article came out of a very pleasant interview over lunch at a Paris bistro with Dick Selwood, one of the EEJournal editors.  So imagine a discussion of ParaSail while we multitasked over bread, cheese, and red wine... ;-)

Sunday, July 22, 2012

Rev 3.0 of alpha release 0.5 available; suppports multi-thread exit/return

We have just released a significant update to the ParaSail compiler and virtual machine, revision 3.0 of the alpha release 0.5:

    http://bit.ly/Mx9DRb

This release is the first to support a "return" or "exit" from inside a parallel construct (such as a concurrent loop or a block with statements separated by "||").  Such a return or exit terminates all of the other picothreads inside of the construct being exited, before returning or exiting with the specified value.  What this means is that you can write parallel loops or other algorithms with multiple picothreads "racing" to find the answer, with the first one to do the "exit loop with xxx;" or "return yyy;" providing the answer, with the other picothreads automatically terminated.  This is a natural way to write certain kinds of parallel algorithms, and now ParaSail makes it quite easy to get the automatic shutdown and cleanup desired for such cases.  A simple example "locked_exit.psl" is included in the "examples" subdirectory of the release, to illustrate such a "multi-thread" exit.

If you have any trouble downloading, installing, or testing out this new release, please don't hesitate to post a response to this note.

Monday, July 2, 2012

Implementation of a directed graph in ParaSail

Below is an implementation of a (pointer-free) directed graph in ParaSail, as promised in the EETimes.com article ParaSail : Less is More with Multicore.
  [ see http://www.eetimes.com/design/embedded/4375616/ParaSail--Less-is-more-with-multicore ]

// Example ParaSail program -- Directed Graph module

// Copyright (C) 2011-2012, AdaCore, New York, NY
// To be used only for Personal, Academic, or Evaluation Purposes;
// Not for Commercial Production Use.
// Report errors at http://groups.google.com/group/parasail-programming-language

interface DGraph<Element is Assignable<>> is
  // Interface to a Directed-Graph module

    type Node_Id is new Integer<1..10**6>;
      // A unique id for each node in the graph

    type Node_Set is Countable_Set<Node_Id>;
      // A set of nodes

    func Create() -> DGraph;
      // Create an empty graph

    func Add_Node(var DGraph; Element) -> Node_Id;
      // Add a node to a graph, and return its node id

    op "indexing"(ref DGraph; Node_Id) 
      {Node_Id in DGraph.All_Nodes()}
      -> ref Element;
      // Return a reference to an element of the graph

    func Add_Edge(var DGraph; From, To : Node_Id)
      {From in DGraph.All_Nodes(); To in DGraph.All_Nodes()};
      // Add an edge in the graph

    func Successors(ref const DGraph; Node_Id) -> ref const Node_Set
      {Node_Id in DGraph.All_Nodes()};
      // The set of successors of a given node

    func Predecessors(ref const DGraph; Node_Id) -> ref const Node_Set
      {Node_Id in DGraph.All_Nodes()};
      // The set of predecessors of a given node

    func All_Nodes(DGraph) -> Node_Set;
      // The set of all nodes

    func Roots(DGraph) -> Node_Set;
      // The set of all nodes with no predecessor

    func Leaves(DGraph) -> Node_Set;
      // The set of all nodes with no successor
end interface DGraph;

class DGraph is
  // Class defining the Directed-Graph module

    interface Node<> is
      // Local definition of Node structure
        var Elem : Element;
        var Succs : Node_Set;
        var Preds : Node_Set;
    end interface Node;

    var G : Vector<Node>;
      // The vector of nodes, indexed by Node_Id

    const Debug : Boolean := #false;

    func Boundary_Set(DGraph; Nodes : Countable_Range<Node_Id>;
      Want_Roots : Boolean) -> Node_Set is
      // Recursive helper for exported Roots and Leaves functions
        if Debug then
            Println("Boundary_Set for " | Nodes.First | ".." | Nodes.Last);
        end if;
        const Len := Length(Nodes);
        case Len of
          [0] =>
            return [];
          [1] =>
            if Want_Roots?
                Is_Empty(Predecessors(DGraph, Nodes.First)):
                Is_Empty(Successors(DGraph, Nodes.First))
            then
                // This is on the desired boundary
                return [Nodes.First];
            else
                // This is not on the desired boundary
                return [];
            end if;
          [..] =>
            // Divide and conquer
            const Half_Way := Nodes.First + Len / 2;
            return 
              Boundary_Set(DGraph, 
                Nodes.First ..< Half_Way, Want_Roots) |
              Boundary_Set(DGraph,
                Half_Way .. Nodes.Last, Want_Roots);
        end case;
    end func Boundary_Set;

  exports

    func Create() -> DGraph is
      // Create an empty graph
        return (G => []);
    end func Create;

    func Add_Node(var DGraph; Element) -> Node_Id is
      // Add a node to a graph, and return its node id
        DGraph.G |= (Elem => Element, Succs => [], Preds => []);
        return Length(DGraph.G);
    end func Add_Node;

    op "indexing"(ref DGraph; Node_Id) -> ref Element is
      // Return a reference to an element of the graph
        return DGraph.G[ [[Node_Id]] ].Elem;
    end op "indexing";

    func Add_Edge(var DGraph; From, To : Node_Id) is
      // Add an edge in the graph
        DGraph.G[ [[From]] ].Succs |= To;
        DGraph.G[ [[To]] ].Preds |= From;
    end func Add_Edge;

    func Successors(ref const DGraph; Node_Id) -> ref const Node_Set is
      // The set of successors of a given node
        return DGraph.G[ [[Node_Id]] ].Succs;
    end func Successors;

    func Predecessors(ref const DGraph; Node_Id) -> ref const Node_Set is
      // The set of predecessors of a given node
        return DGraph.G[ [[Node_Id]] ].Preds;
    end func Predecessors;

    func All_Nodes(DGraph) -> Node_Set is
      // The set of all nodes
        return 1 .. Length(DGraph.G);
    end func All_Nodes;

    func Roots(DGraph) -> Node_Set is
      // The set of all nodes with no predecessor
        return Boundary_Set
          (DGraph, 1 .. Length(DGraph.G), Want_Roots => #true);
    end func Roots;

    func Leaves(DGraph) -> Node_Set is
      // The set of all nodes with no successor
        return Boundary_Set
          (DGraph, 1 .. Length(DGraph.G), Want_Roots => #false);
    end func Leaves;
end class DGraph;

func Test_Graph() is
  // A test program that manipulates a directed graph of univ-enums

    type DGE is DGraph<Univ_Enumeration>;

    func Build_Graph() -> New_G : DGE is
      // A function that builds up a graph of Univ_Enumerations
        New_G := Create();  // Create the empty graph

        const Hello := New_G.Add_Node(#hello);
        const There := New_G.Add_Node(#there);
        const Stranger := New_G.Add_Node(#stranger);

        New_G.Add_Edge(Hello, There);
        New_G.Add_Edge(There, Stranger);
        New_G.Add_Edge(Hello, Stranger);

    end func Build_Graph;
       
    func Print_Nodes(DGE; Nodes : DGE::Node_Set; Indent : Univ_String := " ")
      // Display the elements of a node set, with the given indent
    is
        for S in Nodes loop
            Println(Indent | DGE[S]);
        end loop;
    end func Print_Nodes;

    func Print_Succs(DGE; N : DGE::Node_Id) is
      // Display the successors of a given node
        Println("Successors of " | DGE[N] | " (node " | N | ")");
        Print_Nodes(DGE, DGE.Successors(N));
    end func Print_Succs;

    // Now build the graph and display some info on the graph

    var Gr : DGE := Build_Graph();

    Println("Roots of graph:");
    Gr.Print_Nodes(Gr.Roots());

    Println("Leaves of graph:");
    Gr.Print_Nodes(Gr.Leaves());
    
    Println("All nodes, and their successors:");
    for N in All_Nodes(Gr) forward loop
        Gr.Print_Succs(N);
    end loop;
end func Test_Graph;

Sunday, July 1, 2012

Rev 2.6 of alpha release 0.5 now available

Revision 2.6 of the 0.5 alpha release of the ParaSail compiler and virtual machine is now available:

  http://bit.ly/LZvZc2

This new release includes a number of enhancements, as well as much more stable support for synchronization via concurrent objects.  The enhancements include: conditional expressions (using either "(if X then Y else Z)" or "X? Y : Z" syntax; initial implementation of compile-time checks for race conditions; container "comprehensions" -- an iterator inside a container aggregate, such as:

  [for I in 1..N => I**2]

to create a table of squares.  See the release notes for all the details.