The Programming Language DINO: Expressions Next Previous Contents

5. Expressions

Expressions are constructs denoting rules of computation of a value from other values by the application of operators. Expressions consist of operands and operators. Parentheses may be used to express specific associations of operators and operands.

5.1 Types and Values

All Dino values are first class values, i.e. they can be assigned to a variable, can be passed as a parameter of a function/class, and can be returned by functions. Operators require operands whose values are of given type and return the value of the result type. Most values have a representation in Dino. When a value representation is encountered in an expression during the expression evaluation, the new value is generated.

There are values of structured types, i.e. values which are built from other values. The value of a structured type may be mutable or immutable. A value or sub-value of a mutable value can be changed. An immutable value can not be changed after its generation. You can make a mutable value immutable as a side effect by applying the operator final (the table key is also made immutable as a side effect of writing to the table). In all cases, the operator returns the operand value as the result. If you try to change an immutable value, the exception immutable is generated. You can make a new mutable value as a side effect of applying operator new. The operator returns a new value equal to the operand value.

          Expr = final  Expr
               | new  Expr

Structured value types are also shared value types. This notion means that if two or more different variables (array elements or table elements) refer to the same value and the value is changed through one variable, the value which is referred through the other variables is changed too. There is no difference between the notion "the same value" and the notion "equal values" for non-shared type values. For the shared type operands, equality means that the operands have the same structure (e.g. vectors with the same length) and the corresponding element values are the same.

Examples:

          new 5
          new ['a', 'b', 'c']
          new "abc"
          new tab ["key0" : 10, "key1" : 20]
          final 5
          final ['a', 'b', 'c']
          final "abc"
          final tab["key0" : 10, "key1" : 20]

Dino has the following types of values:

5.2 Designators

There is a special Dino construction called a designator. A designator refers for a vector or table element or for a declaration. If the designator refers to a vector or table element or for a variable declaration, it can stand in the left hand side of an assignment statement. If the designator stands in an expression, the corresponding value is used (the vector/table element value, variable value, function, fiber, or class). When the designator referring to a table element stands up in the left hand side of an assignment statement, its key value becomes immutable.

A designator referring to a vector or table element has the following syntax:

           Designator = (Designator | Call) "["  Expr "]"

The value of the construction before the brackets must be a vector or table. Otherwise, the exception indexop is generated.

If the value of the construction before the brackets is a vector, the value of expression in the brackets (so called index) is converted to an integer value. If this is not possible, the exception indextype is generated. If the index is negative or greater than or equal to the vector length, the exception indexvalue is generated. The value of the designator will be the vector element value with given index (the indexes starts with zero). Examples:

          vector [1]
          vector ["1"]
          vector [1.0]

If the value of the construction before the brackets is a table, the value of expression in the brackets is called key. The value of the designator will be the table element value with the key which is equal to given key. If the element with the given key is absent in the table, the exception keyvalue is generated. Examples:

          table ['c']
          table [10]
          table ["1"]
          table [1.0]

The remaining forms of designator refer to a declaration. See the section Declarations and Scope Rules for a description on how they work.

          Designator = (Designator | Call) "."  IDENT
                     | IDENT

Examples:

          value
          value.f

Dino also has an extended form of the designator described above. This form is called a vector slice:

          Expr = ExtDesignator

          ExtDesignator = Designator {Slice}

          Slice = "["  [Expr] ":" [Expr] [":" Expr] "]"

The slice is applied to a vector, otherwise the exception sliceform is generated. The slice refers for a part of vector given by the start index, the bound, and the step. If they are not integer, the exception slicetype is generated.

The negative bound means counting the index from the vector end. -1 corresponds to the vector length, -2 corresponds to the vector length - 1, -3 corresponds to the vector length -2, and so on. If the bound is more than the vector length, the bound is assumed to be equal to the vector length. If the bound is omitted, it is believed to be equal -1, or in other words, to the vector length.

If the start index is less than zero, the exception sliceform is generated. If the start index is omitted, it is believed to be zero. If the step is omitted, it is believed to be one.

The slice refers to vector elements with indexes start, start + abs (step), start + 2 * abs (step), start + 3 * abs (step) and so on while the index is less than the bound. If the step value is negative, the elements are considered in the reverse order. Examples:

          v[:]    // all vector elements
          v[1:]   // all vector elements except the first one
          v[:-2]  // all vector elements except the last one
          v[::-1] // all vector elements in reverse order
          v[0::2] // all vector elements with even indexes

More one slice can be given after the designator. In this case, next slice is applicable to each element of the previous slice. That means that each element and the next slice should abide by the above rules. The first slice is called the first dimension slice, the second one is called the second dimension slice and so on. Examples of multi-dimensional slices:

          m[:][:]     // all matrix elements
          m[:][::2]   // matrix elements with even columns
          m[::2][::]  // matrix elements with even rows
          m[::2][::]  // matrix elements with even rows and columns

The slices are actually not real reference values. They can be considered as an attribute which exists in a statement at most. If vector with slices is an operand of an operation which has a slice variant, the operation is applied to each element of the vector referenced by the slices in given order. The result is a new vector with the same dimension slices referencing all elements with the step value equal to one.

When a vector with slices is an operand of an operation, the new vector consisting of elements referenced by the slices is created. The new vector will have the same dimension slices referencing for all new vector elements with the step equal one. This is done to avoid unexpected side-effects of function-calls inside a statement with slices.

5.3 Calls

One form of expression is a call of a function, a fiber, or a class. The value of the designator before the actual parameters should be a function, a fiber, or a class. Otherwise, the exception callop is generated. An instance of the block corresponding to the body of the function, fiber, or class is created. The actual parameter values are assigned to the corresponding formal parameters. If the corresponding function, fiber, or class has no default formal parameter args or parameters with default values (see the section Declarations), the number of the actual parameters should be equal to the formal parameter number. Otherwise, a vector whose elements are the remaining parameter values is created and assigned to the parameter args. If there is no corresponding actual parameter for a formal parameter with a default value, the default parameter value (see the section Declarations) is assigned to the formal parameter. After the parameter initialization the statements in the block are executed. If it is the call of a fiber, a new execution thread is created, and the statements of the block is executed in the new thread. The value of call of the fiber is the corresponding thread. It is returned before starting the execution of statements in the new thread.

The execution of the body is finished by reaching the block end or by execution of a return-statement. Finishing of the fiber results in finishing the corresponding thread. The return-statement in a fiber or in class should be without an expression. The call of a class returns the created object. A function call returns the value of the expression in the executed return-statement. Otherwise, the function return value is undefined.

          Expr = Call

          Call = Designator ActualParameters

          ActualParameters = "(" [ Expr { "," Expr } ] ")"

Examples:

          f ()
          f (10, 11, ni, [])
          obj.objf ()

5.4 Operators

Expressions consist of operands and operators. The order in which operators are executed in an expression is defined by their priority and associativity. That means that the expression a op1 b op2 c when the operator op2 has higher priority than op1 is analogous to a op1 (b op2 c). Dino operators have analogous priorities to the ones in the language C. The following Dino operators are placed in the order of their priority (the higher the line on which the operator is placed, the higher its priority).

          !  #  ~  final  new
          *  /  %
          +  -
          @
          <<  >>  >>>
          <  >  <=  >=
          ==  !=  ===  !==
          &
          ^
          |
          in
          &&
          ||
          :
          ?

All binary operators have left associativity in Dino. That means that the expression a op1 b op2 c when operators op1 and op2 have the same priority is analogous to (a op1 b) op2 c. Parentheses may be used to express specific associations of operators and operands.

          Expr = "(" Expr ")"

Most of the Dino operators require the operands to be of given types. If an operand is not of given type, the conversion of it into a value of the type needed may be made. If after the possible conversions the operands are still not of necessary types, the exception optype is generated. The following conversions may be made by default:

Logical operators

Logical operators produce the integer result 1 which means true or 0 which means false. Logical `or' || and logical `and' && are short circuit operators. That means that the second operand is evaluated depending on the result of the first operand. When the operands of the operators are evaluated, the arithmetic conversion is made.

If the first operand of logical `or' is nonzero (integer, long integer, or floating point), the result will be 1. Otherwise, the second operand is evaluated. If the second operand is nonzero, the result will be 1. Otherwise, the result will be 0.

If the first operand of logical `and' is zero (integer, long integer, or floating point), the result will be 0. Otherwise, the second operand is evaluated. If the second operand is nonzero, the result will be 1. Otherwise, the result will be 0.

Logical negation ! makes an implicit arithmetic conversion of the operand. If the operand is zero (integer, long integer or floating point), the result will be 1. Otherwise, the result will be 0.

Operator in checks that there is an element with the given key (the first operand) in the given table (the second operand). If the element is in the table, the result will be 1. Otherwise the result will be 0. If the second operand is not a table, the exception keyop is generated.

          Expr = Expr "||"  Expr
               | Expr "&&"  Expr
               | Expr in  Expr
               | "!"  Expr

Examples:

          !(type (i) == int && type (a) == tab && i >= 0 && i < #a)
          k in t && t[k] == 0
          0.0  || another_try
          0  || another_try

Bit operators

The following operators work on integers (implicit integer conversion is made) and return an integer result. Operators | ^ & ~ denote correspondingly bitwise or, bitwise exclusive or, bitwise and, and bitwise negation of 64-bit integers.

Operators << >>> >> denote correspondingly logical left bit shift, logical right bit shift, and arithmetic right bit shift (with sign extension) of given number (the first operand) by given number of the bits (the second operand). The value of the second operand must be non-negative, otherwise the result is undefined.

          Expr = Expr "|"  Expr
               | Expr "^"  Expr
               | Expr "&"  Expr 
               | Expr "<<"  Expr
               | Expr ">>"  Expr
               | Expr ">>>"  Expr
               | "~"  Expr

Examples:

          (i >> shift) & mask
          i & ~mask | (value << shift) & mask
          i >>> 2
          i << 2

Comparison operators

All comparison operators return a logical value (integer 0 which means false or integer 1 which means true).

Operators equality == and inequality != may make some conversion of the operands. If one of the two operands is string, then the string conversion is applied to the other operand before the comparison. Otherwise, standard arithmetic conversion is applied to the operands if one operand is of an arithmetic type. The operators do not generate exceptions (but the conversions may). The operands are equal if they have the same type and equal values (see the section Types and Values). For instances, functions and classes, the equality requires also the same context.

Operator identity === or unidentity !== returns 1 if the operands have (or not) the same value or 0 otherwise. The operators never generate exceptions and no any conversion is made.

By default the arithmetic conversion is applied to the operands of operators < > <= >=. There is no exception if the operands after the conversion are of integer, long integer, or floating point type. So the operands should be characters, integers, long integers, floating point numbers, or strings representing integers, long integers, or floating point numbers.

          Expr = Expr "=="  Expr  
               | Expr "!="  Expr  
               | Expr "==="  Expr  
               | Expr "!=="  Expr  
               | Expr "<"  Expr
               | Expr ">"  Expr  
               | Expr "<="  Expr
               | Expr ">="  Expr 

Examples:

          10 == 10
          10 === 10
          10 == 10l
          10 == 10.0
          10 !== 10l
          10 !== 10.0
          10 <= 'c'
          p != nil
          'c' == "c"
          10 < "20.0"
          [10, 20] == [10, 20]
          [10, 20] !== [10, 20]

Arithmetic operators

The following operators return integer, long integer, or floating point numbers. Before the operator execution, an implicit arithmetic conversion is made on the operands. The binary operators + - * / % denote correspondingly integer, long integer, or floating point addition, subtraction, multiplication, division, and evaluation of remainder. Unary operator - denotes arithmetic negation. The unary operator + is given for symmetry and it returns simply the operand after the conversion. It can be used for conversion of a string into an integer, a long integer, or floating point number.

          Expr = Expr "+"  Expr
               | Expr "-"  Expr
               | Expr "*"  Expr
               | Expr "/"  Expr
               | Expr "%"  Expr
               | "+"  Expr
               | "-"  Expr

Examples:

          +"0"
          +"10l"
          +"10."
          +"1e1"
          -i
          (value + m - 1) / m * m
          index % bound 

Miscellaneous operators

The Dino conditional expression is analogous to the language C one. An implicit arithmetic conversion is made for the first expression followed by ?. If the value of the expression is non zero (integer, long integer, or floating point value), the second expression with following : is evaluated and it will be the result of the condition expression. Otherwise, the third expression is evaluated and it becomes the result.

The operator # can be applied to a vector or a table. It returns the length of the vector or the number of elements in the table.

The operator @ denotes a concatenation of two vectors into a new vector. Before the concatenation an implicit string conversion of the operands is made.

The remaining operators look like function calls. Operator type returns the expression type. Never is an exception generation possible during the operator evaluation.

The operator char is used to conversion of a value into a character. First, an implicit integer conversion is applied to the operand. The operand should be an integer after the conversion. Otherwise, the exception optype will be generated. The integer is transformed into the character with the corresponding code. If the code is too big to be a character or is negative, the exception erange is generated.

The operator int is used for a conversion of a value into an integer. Implicit integer conversion is applied to the operand. The operand should be an integer after the conversion. Otherwise, the exception optype will be generated. If the code is too big to be an integer, the exception erange is generated.

The operator float is used for a conversion of a value into a floating-point number. The first, an implicit arithmetic conversion is applied to the operand. The operand should be an integer, an long integer, or a floating-point number after the conversion. Otherwise, the exception optype will be generated. An integer is transformed the corresponding floating-point number. The same is done for a long integer but if the number is too big or too small to be a floating-point number, the result is undefined.

The operator vec is used for a conversion of a value into a vector. First, an implicit string conversion is applied to the operand. The optional second expression defines the format used only for the string conversion of a character, an integer, a long integer, a floating point number, or a string. The second parameter value should be a string after an implicit string conversion. The format should not be given for a table. The first operand should be a table or a vector after the conversion. The table is transformed into a new vector which consists of pairs (one pair for each element in the table). The first element of the pair is a key of the corresponding element, and the second one is the element itself. The order of pairs in the result vector is undefined.

The operator tab is used for a conversion of a value into table. First, a string conversion is applied to the operand. The operand should be a vector or a table after the conversion. The vector is transformed into a new table whose elements are equal to the vector elements that have integer keys equal to the corresponding vector indexes.

          Expr = Expr "?"  Expr ":" Expr
               | "#"  Expr
               | Expr "@"  Expr
               | type "(" Expr ")"
               | char "(" Expr ")"
               | int "(" Expr ")"
               | float "(" Expr ")"
               | vec "(" Expr ["," Expr] ")"
               | tab "(" Expr ")"

Examples:

          i < 10 ? i : 10
          #tab ["a", 'b']
          #["a", 'b']
          "concat this " @ "and this"
          type (type)
          type (10)
          char (12)
          vec  (10)
          vec  (10, "%x")
          vec (tab ["1":1, "2":2])
          tab ([1, 2, 3, 4])

Slice Operators

There are slice variants of all operators of mentioned above except for the short circuit operators && and || and the conditional expression.

For unary operators, operator execution on slices creates a new vector structure consisting of elements referenced by the slices in given order, creates the same dimension slices referencing all elements of the new vector structure and execute the operator on each element of the created slices which will be the result of the operator. Examples:

          var v = [1, 2, "3"], m = [[1, 2.], [3, 4, 5]];
          ! v[:];
          - m[1:][::2];
          float (m[:][:]);

Binary operators on the slices are a bit more complicated. If the both operands have slices, they should have the same form (dimension, number of elements in each corresponding sub-vector referenced by the slices), otherwise the exception sliceform is generated. The operator execution can be considered as creating a new vector structure consisting of elements referenced by the slices in and creating the same dimension slices referencing all elements of the new vector structure. The same is done for the second slice. Than the elements of the new vector structure created for the first operand is changed by the result of the operator applied to this element and the corresponding element from the slices created for the second operand. The slices created for the first operand will be the result. Examples:

          var v = [1, 2, 3], m = [[4, 5], [6, 7]];
          v[:] * v[:];
          m[:][0::2] + m[:][1::2];

If only one operand has the slices, the operator execution can be considered as creating a new vector structure consisting of elements referenced by the slices and creating the same dimension slice referencing all elements of the new vector structure. Than the elements of the new vector structure is changed by the result of the operator applied to this element and another operand in given operand order. The created slices will be the result. Examples:

          var v = [1, 2, 3], m = [[4, 5], [6, 7]];
          1 - v[:];
          v[:] * v[:] * 2;
          m[:][0::2] + 1;

Dino has additional unary operators which work only on the slices:

          Expr = ".+"  Expr
               | ".*"  Expr
               | ".&"  Expr
               | ".^"  Expr
               | ".|"  Expr

The are called fold operators. If the operand has no slices, the exception vecform is generated. The operator execution is to apply the corresponing non-fold operator to all elements referenced by the slices. Examples:

          var v = [1, 2, 3], m = [[4, 5], [6, 7]];
          .+ v[:];
          .* v[:];
          .& m[:][:];

5.5 Current block instance

The instance of the block immediately surrounding the current program point can be accessed by using the keyword this.

          Expr = this

Example of usage of this:

          class c (x) { putln (this.x, "===", x); }
          fun f (pub x) { return this; }

5.6 Anonymous Functions and Classes

Instead of declaring a function/class and using its identifier once in an expression, an anonymous function/class can be used. The anonymous functions/classes are regular expressions. They have the same syntax as the corresponding declarations. The only difference is an absence of the function/class identifier:

          Expr = AnonHeader FormalParameters Block

          AnonHeader = [Qualifiers] FuncFiberClass

Examples:

          fun (a) {a > 0;}
          class (x, y) {}
          fiber (n) {for (var i = 0; i < n; i++) putln (i);}

An example of anonymous function usage:

          fold (fun (a, b) {a * b;}, v, 1);

5.7 Try-operator

Dino has two ways to process the exceptions. One way is described in the section Try-block. Another way is to use a try-operator. The try-operator has the following syntax:

          Expr = try "(" ExecutiveStmt [ "," ExceptClassList] ")"

The operator returns non-zero if no exceptions occurs in the statement given as the first operand. The operator returns zero if an exception occurs and its class is a sub-class (see the function isa) of one exception class given by the subsequent operands. If there is no matched operand class, the exception is propagated further (see the try-block description for more details). If the optional subsequent operands are omitted, any occured exception in the statement results in the zero result value.

Here is the example of usage of the try-operator:

            var ln;
            for (; try (ln = getln (), eof);) putln (ln);


Next Previous Contents