Statements

There are five kinds of statements in the Vesta SDL:

Assignment

Assignment statements work more or less as you would expect them to:

four = 2 + 2;

You can optionally specify a type qualifier with the variable identifier to specify the type which should be assigned by the statement:

four : int = 2 + 2;

Vesta follows the C/C++ convention for assignment operators:

four : int = 2;
four += 2;

However the only operators which can be used like this are +, ++, -,and *.

Don't let the presence of assignment statements fool you into thinking of Vesta SDL as an imperative language, because it's really a functional language.  Although an assignment statement appears to have a side-effect (changing the value of a variable), from a language design standpoint it's really just syntactic sugar for introducing a new scope containing a new variable.

Iteration

In Vesta, the only forms of iteration are iteration over the elements of a list and iteration over the elements of a binding.  If iterating over the elements of a list, a foreach loop is begun like this:

foreach elem in l do
  ...

Here, the variable "elem" gets assigned the values of the different elements of list "l" and the body of the foreach loop is executed, once for each list element.

To iterate over a binding, use this syntax:

foreach [ name = val ] in b do
  ...

In each iteration of the loop, the variable "name" is assigned the text value of an identifier in the binding "b" and the variable "val" is assigned the corresponding value.

The body of foreach loops can be either a single statement:

  foreach elem in list do
    total += elem;

Or a block of statements:

  foreach [ name = val ] in binding do
  {
    func_count += if _is_closure(val) then 1 else 0;
    func_map += [ name = _is_closure(val) ];
  };

Don't be fooled by the apparent side-effects of loops into forgetting that Vesta SDL is a functional language.  As with assignments outside of loops, those in loops introduce new dynamic scopes.  Also, don't confuse the behavior of loop bodies, which can change the scope used after the loop, with statement blocks as expressions which act in a more obviously functional way (having no effect other than their return value).

Examples

reverse_list(l: list): list
{
  res: list = <>;
  foreach elt in l do
    res = <elt> + res;
  return res;
};

This function iterates over a list, constructing a list of the same elements in the reverse order.

count_leaves(b: binding): int
{
  res: int = 0;
  foreach [ nm = val ] in b do
    res += if _is_binding(val)
             then count_leaves(val)
             else 1;
  return res;
};

This function treats the passed binding as a tree and counts the number of leaf nodes (nodes which are not themselves bindings).  Note that this function is recursive, which is allowed.

Function Definition

Defining a function assigns a function value to a variable.  When a function is defined, the number of arguments it accepts and the names of individual arguments are defined.  For example, this:

increment(i)
{
  return i + 1;
}

Creates a function which accepts a single argument named i and returns i plus one.  This function is stored in a variable names increment.

When defining a function, you may specify the types of arguments and the type value returned by the function.  Argument and return types can be simple types, type expressions, or previously defined named types.  For example, a more precise definition of increment would be:

increment(i : int) : int
{
  return i + 1;
}

You can also define default values for arguments.  For example, if we wanted increment to be callable with no arguments,  then we could provide a default value for the argument i:

increment(i : int = 0) : int
{
  return i + 1;
}

By assigning i a default value of 0, we make increment() return 1 and increment(increment()) return 2.

When default argument values are given, any arguments following one with a default value must alos have default values.  In other words, this is incorrect and will result in a syntax error:

foo(x: int = 1, y: int): int
{
  // ...
}

Even if this didn't cause an error, the default value for x would never be used (since a value for y is required).  (Many programmers will recognize this restriction, as the same restriction applies to optional parameters in languages like C++ and most dialects of Lisp.)

In addition to defined arguments, every function has a final implicit argument named "." (normally called "dot" and sometimes called "the environment").  When a function is called, if no value is specified for this implicit parameter, it takes the value of dot in the calling scope.  By convention, the value of dot is usually a binding.  For example, consider this function:

foo()
{
   return ./msg;
}

This function assumes that dot will be a binding with a value bound to the name msg.  Evaluating foo([ msg = "Hello"]) would produce the text value "Hello".  If the value of dot in the calling scope was the binding [ msg = <"The fox", "jumped over", "the lazy dog."> ], then evaluating foo() would pass dot implicitly and produce the list <"The fox", "jumped over", "the lazy dog.">.

In general use, the form of a function definition is as follows:

identitifer([ identifier [ : type-expression ] , ... ]
            [ identifier [ : type-expression ] [ = default-value ] , ... ]) [ : type-expression ]
{
  [ statement-list; ]
  return-statement;
}

There are a few other points worth mentioning about function definitions:

Examples

reverse_list(l: list): list
{
  ...
};

This defines a function name "reverse_list" which accepts a single parameter which must be a list.  Its return value is a list.

env_build(bridge_platform: text = "DU4.0")
{
  ...
};

The function "env_build" accepts a single parameter named "bridge_platform" of type text which has a default value of "DU4.0". The return type of the function is undefined.

Type Definition

The Vesta SDL includes a simple mechanism for naming aggreagate types, similar to the C/C++ typedef mechanism.  The basic form of a type definition statement is:

type type-name = type-expression;

(The syntax of type expressions is described elsewhere.)

Examples

type IntBinding = binding(:int);

This defines a type named "IntBinding" which can have any number of names, but all of which must be bound to integer values.

type MapFunc = function(function, list): list;

This defines a type named "MapFunc" which is a function which accepts two parameters: one which is a function and another which is a list. The function of type "MapFunc" produces a return value of type list. (This is the type of the built-in functions _map and _par_map.)

type TestFuncList = list(function(any):bool);

The type TestFuncList is a list of functions which accept one parameter of any type and return a boolean.

There are many more examples of type definitions in the vtypes(5) man page.

return/value

A return statement takes the result of a particular expression and makes it the result of either the enclosing model, user-defined function, or block expression.:

return expression;

value is a synonym for return; the two can be used interchangeably.  (An unstated common convention is to use return for model and function results and value for block expression results.)

The return statement works more or less as anyone familiar with most modern languages would expect, with one important exception: a return statement may only be used as the final statement in a block, and the final statement in a model, user-defined function, or block expression must always be a return statement.  In other words, a return statement cannot be used to exit a function in the middle.


Kenneth C. Schalk <ken@xorian.net>   / Vesta SDL Programmer's Reference