Skip to content

ELENA Programming Manual

Aleksey Rakov edited this page Jan 31, 2024 · 258 revisions

Content

Overview

A programming language is a formidable task to develop and to learn. So encountering a new language you may ask: why another programming language? The short answer is to try it a little differently. Why not to treat the message like a normal language element with which different operations are possible : loading, passing, modifying, dispatching? Could we do it without using reflection all the way? This was my motivation. ELENA is designed to facilitate writing dynamic, polymorphic code, combing elements of dynamic and static languages.

ELENA is a general-purpose language with late binding. It is multi-paradigm, combining features of functional and object-oriented programming. It supports both strong and weak types, run-time conversions, boxing and unboxing primitive types, direct usage of external libraries. Rich set of tools are provided to deal with message dispatching : multi-methods, message qualifying, generic message handlers. Multiple-inheritance can be simulated using mixins and type interfaces. Built-in script engine allows to incorporate custom defined scripts into your applications. Both stand-alone applications and Virtual machine clients are supported

To summarize all of the above, let's name some functions of ELENA:

  • Free and open source (MIT licensed)
  • Complete source code
  • Unicode support (utf-8 / utf-16 / utf-32)
  • GUI IDE & Debugger
  • Optional types
  • Multiple dispatching / multi-methods
  • Support of variadic methods
  • Yieldable methods
  • Closures
  • Mixins
  • Type interfaces / conversions
  • Class / code templates
  • Script Engine
  • Cross-platform tool set (x86, x86-64, PPC64le and AARCH64)

Language Environment

ELENA Project provides the rich set of tools to develop, debug and examine the code written in the language:

Executable Description
elena-cli (32bit) / elena64-cli (64bit) Command-line compiler
elena-ide (32bit) / elena64-ide (64bit) IDE & debugger
ecv-cli (32bit) / ecv64-cli (64bit) Byte-code disassembler
asm-cli (32bit) / asm64-cli (64bit) Machine / byte code assembler
elenavm60.dll / libelenavm60.so (32bit) / elenavm60_64.dll / libelenavm60_64.so (64bit) Virtual machine
elenart60.dll / libelenart60.so (32bit) / elenart60_64.dll / libelenart60_64.so (64bit) Run-time library
elenasm60.dll / libelenasm60.so (32bit) / elenasm60_64.dll / libelenasm60_64.so (64bit) Script engine
elt-cli.exe (Win32) / elt64-cli.exe (64bit) Virtual machine console

Supported CPUs

Currently x86, x86-64, Aarch64 and PPC64le are supported.

Supported platforms

CPU OS
x86 Windows / Linux
x86-64 Windows / Linux
Aarch64 Linux
PPC64le Linux

The following project targets are supported:

Target platform Description
lib a library
lib_64 a 64 bit library
console Win32 / Linux I386 CUI (console user interface) stand-alone STA
console_64 Win64 CUI (console user interface) stand-alone STA
gui Win32 GUI (graphical user interface) stand-alone STA
vm_console Win32 / Linux I386 CUI (console user interface) virtual machine client STA
vm_console_64 Win64 CUI (console user interface) virtual machine client STA
mta_console Win32 CUI (console user interface) stand-alone MTA

where STA means single-thread application, MTA - multi-thread application

Installing

The latest version of the language can be always found at GitHub or Sourceforge.

The simplest way to install the language is to use a setup. The zip files are provided as well. To install from them, just unzip all the files into a directory you want. You will need to add a path to BIN folder to system environment (e.g. <app root>\bin). The language requires Visual C++ Redistributable for VS 2019.

To open, compile or debug the programs and libraries use ELENA GUI IDE (<app root>\bin\elena-ide.exe) or ELENA Command Line Compiler (<app root>\bin\elena-cli.exe).

In ELENA IDE you can select File->Open->Open Project option and open an appropriate project file (*.prj). Then select Project->Compile option to compile the project, Debug->Step Over or Debug->Step Into to debug it or Debug->Run to execute the program.

A number of tutorials with complete source code can be found here

Language Elements

We will start to learn ELENA by getting familiar with the basic program elements : programs, variables, symbol, classes, control flows.

Any ELENA program consists of source files (with .l extension) and the main project file (with .prj extension). The project file contains the main information how the source files will be compiled and what will be the result of the compilation: a library or an executable. The simplest project type is console one : it will generate a stand-alone console program.

The easiest way to generate a new project is to use IDE. Let's select File->New->Project

New Project Dialog

We have to choose the project type - console, enter the root namespace - example1 and specify the target executable name - example1.

The project should contain at least one source file - File->New->Source File. After saving the project we are ready to write our first program.

Program Structure

A simplest ELENA project consists of a source file located in the project root folder.

NOTE : Source files are UTF8 encoded. The project file is an XML document with configuration root node. In the current implementation the project file contains no xml namespace attributes

E.g. the following project

<configuration>
    <project>
        <template>console</template>
        <namespace>example1</namespace>
        <executable>example1.exe</executable>
    </project>
    <files>
        <module>
            <include>myprogram.l</include>
        </module>
    </files>
</configuration>

will produce example1.nl module.

Typically the project compilation produces a single module file (.nl). But it is possible to specify multi-library output:

<configuration>
    <project>
        <template>console</template>
        <namespace>myproject</namespace>
        <executable>example1.exe</executable>
    </project>
    <files>
        <module>
            <include>myprogram.l</include>
        </module>
        <module name="subproject">
            <include>subproject\file2.l</include>
        </module>
    </files>
</configuration>

In this case two libraries will be generated - myproject.nl and myproject.subproject.nl

A project file contains the project settings, source files, forwards and so on. The project may be based on a project template. In this case it inherits all the parent settings except the overloaded ones. The template may be based on another one and so on.

Entry point

A program entry is a main program code which is called at the program startup (after some preparations). When the last statement is executed the program is terminated. Any exception happens in the main code (or in nested one) will be caught by the system handler allowing graceful program exit.

To create an entry point we have to declare a public function with a name program and no arguments in the project root namespace (in the simplest case in any of the project source files). Writing a function is quite straightforward for anyone familiar with C-like languages:

public program()
{
}

The code is written inside curly brackets and consists of statements. A statement has to be terminated with semicolon (except the last one, where the semicolon is optional). A typical statement is a method call (a message sending), an operation or a control-flow statement. In our case we will print a text on the system console using console symbol. The message argument is a string literal (enclosed in double quotes):

public program()
{
   console.writeLine("Hello World!");
}

The output will be:

Hello World! 

Variables

A variable is a named place in the program memory (for example in the function stack) which contains a value. In the simplest case it is a reference to an object or null pointer - nil value. A variable name can contain any UTF-8 symbols (except special ones, white-spaces; the leading character cannot be a digit) and is case-sensitive. There are no reserved keywords. Variables with duplicate names (declared in the same scope) are not allowed.

public program()
{
    var myFirstVariable := "Hello";
    var Δ := 1;
}

The new variable declaration starts with an attribute var, the bound value are placed after an assigning operator (NOTE: assigning and equivalent operators are not the same in ELENA). The value can be the result of the expression as well.

The declaration and assigning can be separated:

public program()
{
    var myFirstVariable;         // declaring a new variable myFirstVariable
    myFirstVariable := "Hello";  // assigning a value
    var Δ := 1;                  // declaring and assigning in the same statement
    Δ := Δ + 1;
}

If a variable is declared without an assignment, its value is nil.

A variable type is optional in ELENA. If the type is not explicitly stated the variable is type-less (or more exactly its type is system'Object, the super class). It means the type is not checked and any value can be assigned.

    var v := "Hello";
    
    v := 3;

In reality there are many situations when we do care about the variable types. So let's create a strong typed variable. The required type (a class name or alias) should be written before the variable name:

    var string v := "Hello";

where string is an alias of system'String class.

var attribute in this case is optional and we could simplify the code:

    string v := "Hello";

The variable type restricts the possible values of the variable. The biggest difference with statically-typed languages that the type-check happens in run-time (though the compiler will warn that no conversion routine was found if both variables are strong-typed). So for example the following code can be compiled but will generate a run-time error:

public program()
{
    var o := 2;
    string v := o;
}

and the output is:

system'IntNumber : Method typecast:#cast[1] not found
Call stack:
sandbox'program.function:#invoke:sandbox.l(4)
system'$private'entry.function:#invoke:app.l(5)
system'$private'entrySymbol#sym:app.l(13)

The value can be successfully converted if an appropriate conversion method is declared:

var o := 2;
real r := o;

In this case system'IntNumber class supports a conversion to system'RealNumber type.

The variable type can be automatically deduced from its initializer using auto attribute:

auto r := 1.2;

the variable type, system'RealNumber (or real), is defined by an assigned value - a numeric literal - 1.2

Numbers

Numbers are one of the most essential data types of a programming language. ELENA supports both integers and floating-point numbers. These are:

Integer Types

Alias Type Signed? Number of bits Smallest value Largest value
byte system'ByteNumber No 8 0 2^8 - 1
short system'ShortNumber Yes 16 -2^15 2^15-1
int system'IntNumber Yes 32 -2^31 2^31-1
uint system'UIntNumber No 32 0 2^32-1
long system'LongNumber Yes 64 -2^63 2^63-1

Floating-point types

Alias Type Number of bits
real system'RealNumber 64

Numbers are primitive built-in types. They could be stored directly in the program memory or used as immediate values (numeric literals). Arithmetic and relational operations are natively supported. Nevertheless, thanks to just-in-time boxing they can be used as normal objects without paying much attention:

import extensions;

public program()
{
   int n := 2;
   console.printLine(n);
}

The result is

2

In the code above the first operation is done directly (copying immediate value into the memory) and the second one uses a boxed value (boxing a number into system'IntNumber object and passing it to an extension method extensions'outputOp.printLine[]). The appropriate types are called primitive wrappers. When it is required the wrapper value can be unboxed back into the memory.

Numeric literals

ELENA supports the following numeric literals:

   int  n1  := 2;             // a 32bit integer literals
   int  n2 := -23; 
   int  x1 := 0Fh;            // a signed 32bit hexadecimal integer        
   uint x2 := 0FH;            // an unsigned 32bit hexadecimal integer        
   long l1 := 1234567890123l; // a 64bit integer literals
   long l2 := -233544364345l;
   real r1 := 23.2;          // a floating-point literals
   real r2 := 1.2e+11;

Numeric conversions

Numbers can be implicitly converted into each other. We can assign an integer to a long integer or a floating-point number. But the opposite is not always possible.

    long l := 123l;
    real r := 123.0;
    
    int n1 := l; // works
    int n2 := r; // fails        

For unsigned, short or byte integers, both conversions are possible with a loss of information in some cases.

    uint x := 0FFFFFFFEH;
    int  n := 300;
    
    short s := n; // works
    byte  b := n; // works, but the value is 44
    int  sn := x; // works, but the value is -2 

Mathematical Operations and Functions

ELENA provides a basic set of operations with primitives. It is easily extended with a help of methods declared in appropriate classes and extensions.

Arithmetic Operators

The following arithmetic operators are supported on all primitive numeric types:

Expression Name
x + y Addition operator
x - y Subtraction operator
x * y Multiplication operator
x / y Division operator

The operands can be of different types. The result of the arithmetic operation, in this case, is the biggest operand type (byte < short < int < long < real). E.g. when we sum an integer and a floating-point numbers the result is floating-point. The division of two integer types are always an integer. If one of the operands is a floating-point number the division is floating point one.

console
   .writeLine(3*4)
   .writeLine(3*4.0r)
   .writeLine(5/2)
   .writeLine(5.0/2);

The result is:

12
12.0
2
2.5

Several operation can be used together:

 console.writeLine(1+2*3.5-3l/2);

Bitwise Operators

Bitwise operators are supported only by integer numbers.

Expression Name
x && y bitwise and operator
x || y bitwise or operator
x ^^ y bitwise xor operator
x $shl y bitwise shift left operator
x $shr y bitwise shift right operator

Similar the different integer types can be used and the result is the biggest operand type.

console
   .writeLine(4 & 2)
   .writeLine(3 | 10000000000l)
   .writeLine(5 ^ 3)
   .writeLine(5 $shl 2)
   .writeLine(5 $shr 1);

The result is:

0
10000000003
6
20
2

Assignment Operators

There are four arithmetic assignment operators to simplify the operations with the left hand operands:

Expression Name
x += y add and assign operator
x -= y subtract and assign operator
x *= y multiply and assign operator
x /= y divide and assign operator

For example

x += y

is a shorthand for

x := x + y

The left-hand operand should always be a variable.

Numeric Comparisons

Standard comparison operations are defined for all the primitive numeric types:

Expression Name
x == y equality
x != y inequality
x < y less than
x > y greater than
x <= y less than or equal to
x >= y greater than or equal to

The result of a comparison is Boolean values : true or false.

import extensions;
  
public program()
{
    console
       .printLine(2.0r == 2)
       .printLine(2 == 3)
       .printLine(2 < 3)       
       .printLine(2 > 3)
}

The results are:

true
false
true
false

Operator Precedence

Operators are ordered in the following manner, from highest precedence to lowest:

Category Operators
Multiplication *, /, $shl, $shr
Addition +, -
Comparisons ==, !=, <, >, <=, >=
Bitwise &&, ||, ^^

Numerical Conversions

Numbers can be converted using either implicit or explicit conversions. Implicit conversion is done automatically but part of the information can be lost. Explicit conversions check if the value will be overflown and generate exceptions.

The recommended way is to use conversion extension methods declared in extensions module

Method Name Description
toByte() converts the target to system'ByteNumber type
toShort() converts the target to system'ShortNumber type
toInt() converts the target to system'IntNumber type
toUInt() converts the target to system'UIntNumber type
toLong() converts the target to system'LongNumber type
toReal() converts the target to system'RealNumber type

Here several examples:

import extensions;

public program()
{
    int n := 3;
    
    byte  b := n.toByte();
    short s := n.toShort();
    long  l := n.toLong();
    real  r := n.toReal();  
}

If a conversion leads to an overflow an exception is generated:

int n := 300;

byte  b := n.toByte();

The output will be:

An index is out of range
Call stack:
system'Exception#class.raise[1]:exceptions.l(25)
system'byteConvertor.static:convert<'IntNumber,'$auto'system@Reference#1&system@ByteNumber>[3]:convertors.l(40)
system'byteConvertor.static:convert<'IntNumber,'$auto'system@Reference#1&system@ByteNumber>[3]:convertors.l(47)
extensions'byteConvertOp.function:toByte<system'Object>[1]:convertors.l(142)
sandbox'program.function:#invoke:sandbox.l(6)
system'$private'entry.function:#invoke:app.l(5)
system'$private'entrySymbol#sym:app.l(13)

Methods and functions

The representative classes greatly extends a set of primitive operations. The functionality is declared either in the proper class or in one of its extensions. For convenience the functions can be used as well.

Operations with bit-wise masks

Method Description
allMask(mask) Returns true if all the mask bits are set
anyMask(mask) Returns true if any of the mask bits are set

For examples the code:

import extensions;

public program()
{
    console
        .printLine("8.anyMask(15)=", 8.anyMask(15))
        .printLine("8l.allMask(15)=", 8l.allMask(15))
}

will generate the following result:

8.anyMask(15)=true
8l.allMask(15)=false

Operations with the number sign:

Method Description
Absolute Returns the absolute value
Inverted Returns the inverted value
Negative Returns the negated value

These properties can be used to return negative, positive or inverted values:

import extensions;

public program()
{
    int r := -123;
    
    console
        .printLine(r,".Inverted = ", r.BInverted)
        .printLine(r,".Negative = ", r.Negative)
        .printLine(r,".Absolute = ", r.Absolute)
}

The result is:

-123.Inverted = 122
-123.Negative = 123
-123.Absolute = 123

When we have to check if our number is positive, negative, zero, odd and so on, these extension methods can be useful:

Method Description
isOdd() Returns true if the number is odd, otherwise false
isEven() Returns true if the number is even, otherwise false
isZero() Returns true if the number is zero, otherwise false
isPositive() Returns true if the number is positive, otherwise false
isNegative() Returns true if the number is negative, otherwise false
isNonnegative() Returns true if the number is non negative ( >= 0), otherwise false

The usage is quite straightforward:

import extensions;

public program()
{
    int n := 2;
    
    console
        .printLine(n," is odd : ", n.isOdd())
        .printLine(n," is even : ", n.isEven())
        .printLine(n," is zero : ", n.isZero())
        .printLine(n," is positive : ", n.isPositive())
        .printLine(n," is negative : ", n.isNegative())
}

with the following result:

2 is odd : false
2 is even : true
2 is zero : false
2 is positive : true
2 is negative : false

Modulo and real division of integer numbers

Method Function Description
mod(operand) modulo(loperand,roperand) An Integer remainder
realDiv(operand) - A float-based division

A modulo operation is implemented in ELENA with a help of an extension method mod

import extensions;

public program()
{
    console.printLine("5 % 2 = ", 5.mod(2))
}

The result is as expected one:

5 % 2 = 1

Instead of extension method we can use an appropriate function declared in extensions'math module

import extensions;
import extensions'math;

public program()
{
    console.printLine("5 % 2 = ", modulo(5, 2))
}

A fraction of two integer numbers is always an integer. If we need an exact result we can use realDiv extension:

import extensions;

public program()
{
    console.printLine("5 / 2 = ", 5.realDiv(2))
}

The output will be:

5 / 2 = 2.5

Operation with floating-point numbers

Method Function Description
Rounded Returns the rounded number
RoundedInt Returns the rounded integer number
Integer Truncates the fraction part
IntegerInt Truncates the fraction part and returns an integer
frac() frac(operand) Returns only the fraction part
ceil() ceil(operand) Returns the smallest integer that is greater than or equal to the operand
floor() floor(operand) Returns the largest integer that is smaller than or equal to the operand
truncate(precision) truncate(operand,precision) Rounds the number to the provided precision
Reciprocal Returns a number obtained by dividing 1 by operand

We can use either extension methods (declared in system'math) or functions (declared in extensions'math):

import extensions;
import system'math;
import extensions'math;

public program()
{
    console
        .printLine("foor(5.6)=", floor(5.6r))
        .printLine("ceil(5.6)=", 5.6r.ceil())
}

The code produces the following output:

foor(5.6)=5.0
ceil(5.6)=6.0

Mathematical functions

Method Function
power(operand) power(loperand,roperand)
sqr() sqr(operand)
sqrt() sqrt(operand)
exp() exp(operand)
ln() ln(operand)
sin() sin(operand)
cos() cos(operand)
tan() tan(operand)
arctan() arctan(operand)
arcsin() arcsin(operand)
arccos() arccos(operand)
log2() log2(operand)
log10() log10(operand)

Similar both extension methods and functions can be used:

import extensions;
import system'math;
import extensions'math;

Pi = RealNumber.Pi;

public program()
{
    console
        .printLine("sin(π/3) = ",(Pi / 3).sin())
        .printLine("cos(π/3) = ",cos(Pi / 3))
        .printLine("tan(π/3) = ",tan(Pi / 3))
}

The result is:

sin(π/3) = 0.8660254037844
cos(π/3) = 0.5
tan(π/3) = 1.732050807569

Converting to radians, degrees

Method Description
Radian Converts to radians
Degree Converts to degree

Strings

String is a sequence of characters representing some text information. The way how to interpret these characters depends on the text encoding. ELENA supports both UTF-8 (system'String, or string alias) and UTF-16 (system'WideString,or wide alias) encoded strings.

Strings are immutable. To modify its content a new string should be created.

A string can be considered as a partial array of UTF-32 characters. It means that for some indexes no character can be returned (an exception will be generated). The reason is UTF-8 / 16 encoding (the character can take more more than one element in the array). When our text is plain ASCII it makes no difference. But for example Russian (for a string) or Chinese (for a wide string) we have to take this feature into account.

Characters

A character is a 32-bit primitive value represented by system'CharValue class (char alias). It can be converted into a 32 bit integer number and vice versa. It supports comparison operations both with another characters and with numbers.

import extensions;
 
public program()
{
    auto s := "♥♦♣♠";    
    
    char ch := s[0];
    int  code := ch.toInt();
    
    console.printLine(ch);
    console.printLine(code);
    console.printLine(ch == code);
}

The result is:

♥
9829
true

ELENA supports character literal constant as well:

console.printLine($9829); // character literal

which will be printed like this:

String literals

The string literals are enclosed in double quotes:

string stringConstant := "Привет Мир"; // UTF-8 encoded constant
wide wideConstant   := "你好世界"w;     // UTF-16 encoded constant

If the string itself contains double quote it should be doubled:

console.printLine("The string can contain a symbol """);

The output is:

The string can contain a symbol "

Multi-line string is supported as well:

console.printLine("This is a string with
two lines");

The result:

This is a string with
two lines

An empty string can be a string literal without content or a constant emptyString:

console.printLine("");
console.printLine(emptyString);

Reading string content

If you want to extract a character from a string, you index into it using an array operator. The index of the first element is 0.

Let's print a first, second and the last characters of the given string. As it was said above for the plain English text, it is quite straight-forward:

import extensions;

public program()
{
    auto s := "Hello";    
    
    console.printLine(s[0]); // printing the first element
    console.printLine(s[1]); // printing the second element
    console.printLine(s[s.Length - 1]); // printing the last element
}

The output is:

H
e
o

Let's try it with Russian word:

auto s := "Привет";    

console.printLine(s[0]); // printing the first element
console.printLine(s[1]); // printing the second element
console.printLine(s[s.Length - 1]); // printing the last element

There error is raised for the second element:

П
Invalid operation
Call stack:
system'Exception#class.raise[1]:exceptions.l(25)
system'$intern'PrimitiveOperations.static:readUTF32<'$auto'system@Array#1&system@ByteNumber,'IntNumber,'$auto'system@Reference#1&system@IntNumber>   [4]:primitives.l(70)
system'$intern'PrimitiveOperations.static:readUTF32<'$auto'system@Array#1&system@ByteNumber,'IntNumber,'$auto'system@Reference#1&system@IntNumber>[4]:primitives.l(139)
system'String.static:at<'IntNumber,'$auto'system@Reference#1&system@CharValue>[3]:strings.l(326)
system'String.static:at<'IntNumber,'$auto'system@Reference#1&system@CharValue>[3]:strings.l(326)
sandbox'program.function:#invoke:sandbox.l(8)
system'$private'entry.function:#invoke:app.l(5)
system'$private'entrySymbol#sym:app.l(13)

Why? Because in UTF-8 a russian character is encoded with two bytes (two elements of String array). It means that the second character must be read from third, and not from the second position.

We can actually fix the problem using UTF-16. Now every symbol is encoded in only one array element.

auto s := "Привет"w;    

console.printLine(s[0]); // printing the first element
console.printLine(s[1]); // printing the second element
console.printLine(s[s.Length - 1]); // printing the last element

And the output is:

П
р
т

But for the Chinese word it will not work.

To correctly read the next character, we have to use a character Length (for strings) or WideLength (for wide strings) properties:

auto s1 := "Привет";    
auto s2 := "你好世界"w;    

console.printLine(s1[s1[0].Length]);     // printing the second element of UTF-8 string
console.printLine(s2[s2[0].WideLength]); // printing the second element of UTF-16 string

Enumerators can be used to simplify the task:

auto s := "Привет";    
auto it := s.enumerator();   // creating a string enumerator

it.next();                   // going to the first element
console.printLine(*it); // printing the current element  
it.next();                   // going to the second element
console.printLine(*it); // printing the current element  

The output as expected:

П
р

To read the last one we have go to the end one by one. Instead let's use an extension method LastMember declared in system'routines module:

import extensions;
import system'routines;

public program()
{
    auto s1 := "Привет";    
    
    console.printLine(s1.LastMember);
}

The output is

т

Basic operations and functions

You can compare two strings:

console.printLine("""string1"" < ""string2""=", "string1" < "string2");
console.printLine("""string1"" > ""string2""=", "string1" > "string2");
console.printLine("""string1"" == ""string2""=", "string1" == "string2");

with the following result:

"string1" < "string2"=true
"string1" > "string2"=false
"string1" == "string2"=false

Several strings or a string and a char can be concatenated. The result of the operation is always a new string.

import extensions;

public program()
{
    console.print("Hello" + " " + " World " + $10);
}

The result will be

Hello  World 

We can insert a sub string into or delete it from given string:

console.printLine("Hillo World".delete(1,1).insert(1, "e"));

The output is:

Hello World

The first operand is a position in the given string (note that we can corrupt the string if a sub string will be inserted into multi-element character). The second one is the length of the sub string for delete and a sub string for insert.

We can find a position of a sub string in the given one.

console.printLine("Hello World".indexOf(0, " "));
console.printLine("Hello World".indexOf(0, "!"));

The result will be

 5
 -1

The first operand is starting index and the second one is the sub string to be found. The method will return the index of the first occurrence of the second argument or -1.

We can return a sub string from the given one:

console.printLine("Hello World".Substring(6,5));

and the result is:

World

Similar to the examples above the first index is a position of the sub string and the second one is its length.

The exception will be generated if the index lays outside the target string for these operations.

There is a number of extension methods for padding and trimming strings

Extension Description
trimLeft() Removes all leading occurrences of the given character
trimRight() Removes all tailing occurrences of the given character
trim() Removes all leading and tailing occurrences of the given character
padLeft() Returns a new string of a specified length in which the beginning of the current string is padded with the given character
padRight() Returns a new string of a specified length in which the end of the current string is padded with the given character

Conversion to and from strings

Every ELENA class supports toPrintable() method which returns a string that represents the object. By default it is a class name:

import extensions;

public program()
{
    console.printLine(console.toPrintable());
}

The output will be the class name ($private prefix indicates the class is private):

system'$private'Console

Basic types override this method and return its text value:

import extensions;

public program()
{
    console.printLine(12.toPrintable());
    console.printLine(23.4r.toPrintable());
}

with the result:

12
23.4

Alternatively we can use toString() extension:

    console.printLine(12.toString());
    console.printLine(23.4r.toString());

Similar a string supports the conversion to basic types:

int n := "12".toInt();
real r := "23.4".toReal();

The direct conversion between a character and an integer is possible, using extensions toInt() and toChar()

import extensions;

public program()
{
    auto ch := $78;
    int code := ch.toInt();
    
    console.printLine("code of ", ch, " is ", code);
}

And the result will be:

code of N is 78

Functions

In ELENA functions are objects which handle a special message function and can be declared as function literals. They are first-class functions and can be passed, stored either as weak or strong typed ones. Functions can be stateless or memory allocated (closures). A special type of functions - extension ones are supported and can handle the data without actually storing them (forming a temporal mix-in).

A function can be named or anonymous. A named one can be public or private. Anonymous (or function literal) one can encapsulate the referred variables. They can be assigned to variables and handled as normal objects.

import extensions;

F(x,y)
{
    // returning the sum of two arguments
    ^ x + y
}

public program()
{
    // using named private function
    console.printLine(F(1,2));
    
    // declaring and assigning anonymous function
    var f := (x,y){ ^ x * y};
    // invoking a function
    console.printLine(f(1,2));    
}

The result is:

3
2

Returning operator

^ operator terminates the code flow and returns the given expression. The operator must be the last one in the scope.

In general, all functions (and class methods) return a value. If the value is not specified, built-in variable self (referring an instance of a method owner) is returned.

Shorthand function declaration

A function body is described inside the curly brackets. If the function contains only returning operator the body can be simplified:

F(x,y)
   = x + y;

Anonymous functions can use => operator:

    var f := (x,y => x * y);

Closures

Variables declared outside the function scope can be referred inside it. The appropriate boxing will be done automatically.

import extensions;

public program()
{
    var n := 3;
    
    var f := (x => n + x );

    console.printLine(f(1))
}

The result will be:

4

Strong-typed arguments

The function arguments can be weak or strong-typed.

import extensions;

Test(int x, int y)
    = x.allMask(y);

public program()
{
    console.printLine(Test(11,4));

    var test := (int x, int y => x.anyMask(y));
    console.printLine(Test(12,4));
}

The result is:

false
true

We can use variadic arguments

import extensions;

Sum(params int[] args)
{
    int sum := 0;
    for (int i := 0; i < args.Length; i++)
    {
        sum += args[i]
    };
    
    ^ sum
}

public program()
{
    console.printLine(Sum(1));
    console.printLine(Sum(1,2));
    console.printLine(Sum(1,2,3));
    console.printLine(Sum(1,2,3,4));
}

The result will be:

1
3
6
10

When an argument can be changed inside the function it should be passed with ref prefix:

import extensions;

Exchange(ref int l, ref int r)
{
    int t := l;
    l := r;
    r := t;
}

public program()
{
    int x := 2;
    int y := 3;
    
    console.printLine(x,",",y);
    
    Exchange(ref x, ref y);
    
    console.printLine(x,",",y)
}

The result is

2,3
3,2

First class messages

First class messages are special type of functions. They are used to dynamically invoke messages and extensions. The messages are first-class functions, they can be stored, passed or created.

Messages

In ELENA a message can be invoked using a special function system'Message. The first parameter is the message target. The rest is a message argument list. The number of arguments should match the message parameter counter minus one (the first argument is self reference). The function returns the result of invoked operation. If no handler is found the exception is raise.

The message function can be created dynamically or using a message literal. The message literal consists of a prefix attribute mssg, the message name and the number of parameters enclosed in the square brackets:

var message := mssg writeLine[2];
message(console,"Hello");

The result is:

Hello

Extension messages

An extension message is invoked by system'ExtensionMessage. It contains the message value and a reference to the extension class. Similar extension literal consists of a prefix attribute mssg, the message name, the extension reference in angle brackets and the number of parameters enclosed in the square brackets:

var n := RealNumber.Pi / 2;
var f := mssg sin<system'math'mathOp>[0];

console.printLine(f(n));

The result is:

1.0

Message names

If the number of arguments can be different we can use the message name function. The message name literal starts with a prefix subj and a message name. The argument counter is not included.

var m := mssg writeLine;
m(console,"Hello");

The result is:

Hello

Control Flow

ELENA supports rich set of control flow constructs : branching, looping, exception handling. They are implemented as code-templates and can be easily extended. Branching and looping statements are controlled by boolean expressions. Though branching operator can deal with non-boolean result as well. Exceptions are used to gracefully deal with critical errors. Exceptions can contain a stack trace to identify the error source.

Boolean type

A BoolValue (alias bool) is a boolean type that has one of two possible values : true and false. It is a typical result of comparison operations. A boolean value is required by control flow statements (though non-boolean result can be used as well, providing a conversion operation exists). It supports typical comparison and logical operations:

console
    .printLine("true==false : ",true == false)
    .printLine("true!=false : ",true != false)
    .printLine("true and false : ",true && false)
    .printLine("true or false : ",true || false)
    .printLine("true xor false : ",true ^^ false);

The results are:

true==false : false
true!=false : true
true and false : false
true or false : true
true xor false : true

A boolean inversion is implemented via Inverted property:

console.printLine("not ",true,"=",true.Inverted)

or using the operator:

console.printLine("not ",true,"=",!true)

and the result is:

not true=false

A boolean value can be used directly for branching operation. The message if with two functions without arguments can be send to it. true value will execute the first function, false - the second one.

import extensions;

public program()
{
    var n := 2;
    
    (n == 2).if({ console.printLine("true!") },{ console.printLine("false!") });
}

with the result:

true!

iif message is supported as well: true value will return the first argument, false - the second one

var n := 2;

console.printLine((n != 2).iif("true", "false") )

The output is as expected:

false

Branching operator

ELENA supports built-in branching operator ?. It requires that an loperand is a boolean value. True-part argument follows the operator immediately. Optional false-part is introduced with colon:

import extensions;

public program()
{
    var n := 2;
    
    (n == 2) ? { console.printLine("n==2") };
    (n == 3) ? { console.printLine("n==3") } ! { console.printLine("n!=3") }
}

The following result will be printed:

n==2
n!=3

If right operands are not functions the operator returns true-part if the loperand is true and false-part otherwise.

var n := 2;

console.printLine(n == 2 ? "n==2" : "n!=2");

with the similar result:

n==2

Branching statements

ELENA does not reserve keywords for the control statements. But there are set of code templates that help make the code more readable. This list can be extended by programmers themselves.

Let's start with if-else, known for many.

var n := 2;
if (n == 2)
{
    console.printLine("n==2")
}
else
{
    console.printLine("n!=2")
}

the result is

n==2

if we need only true-part, we can skip the else block.

if (n == 2)
{
    console.printLine("n==2")
}

Alternatively only else part can be written:

ifnot (n == 2)
{
    console.printLine("n!=2")
}

If the code brackets contains only single statement we can write the statement directly - if it is the last block:

if(n == 2)
   console.printLine("n==2");

Looping statements

ELENA provides several type of loop constructs : for, while, until, do-until, do-while.

FOR loops is used to execute the loop body for the specified iteration. It consists of initialization, condition and iteration expressions separated by semicolon and the main loop body:

import extensions;

public program()
{
    for (var i := 0; i < 5; i++)
    {
        console.printLine(i)
    }
}

The first expression declares and initializes the loop variable, the second one is checked if the variable is inside the iteration range and the third one is increased the loop variable after the main loop body is executed.

The result will be:

0
1
2
3
4

Similar if the loop body contains only single statement we can omit the brackets:

import extensions;

public program()
{
    for (var i := 0; i < 5; i++)
        console.printLine(i)
}

If we need to execute an iteration step before the condition check we can skip the last expression. In this case the first expression will both initialize and iterate the loop variable on every step. The condition (second expression) will work similar, the loop continues until a false value is encountered. As a result the iteration step is guarantee to be executed at least once.

import extensions;

public program()
{
    var sum := 0;
    for(var line := console.readLine(); line != emptyString)
    {
        sum += line.toInt()
    };
    
    console.printLine("The sum is ", sum)
}

In this example we read numbers from the console until an empty line is encountered, and print the sum. A method readLine reads the next line of characters from the console. emptyString is an empty string literal constant. An extension method toInt converts a string to an integer.

The output looks like this:

1
3
5

The sum is 9

WHILE is a classical loop construct, it executes the code while the condition is true. Let's write the enumeration loop. Enumerators are special objects which enumerates collections, returning their members one after another. We will enumerate a string:

import extensions;

public program()
{
    auto s := "♥♦♣♠";
    auto e := s.enumerator();
    while (e.next()) {
        console.printLine(*e)
    }
}

The result will be:

♥
♦
♣
♠

The opposite construct is UNTIL. It repeats the code until the condition is true. Let's repeat the code while the number is not zero:

import extensions;

public program()
{
    var n := 23;
    until(n == 0)
    {
       console.printLine(n);
     
       n /= 3
    }
}

The output will be:

23
7
2

It is clear that both loop constructs are interchangeable. The choice can depend on rather semantic meanings : to underscore one condition over another one.

If we need to execute the loop at least once whatever condition is, we have to use DO-WHILE or DO-UNTIL. Let's calculate the factorial of 5:

import extensions;

public program()
{
    int counter := 5;
    int factorial := 1;

    do {
        factorial *= counter;
        counter -= 1
    }
    while(counter > 0);
    
    console.printLine(factorial)
}

Note that we have to put colon after while token

The result as expected:

    120

Exception handling

Exception handling is designed to deal with unexpected situations during the program executing. For dynamic language the typical unexpected situation is sending a message to the object which does not handle it (no appropriate method is declared). In this case MethodNotFound exception is raised. Another notable examples are : dividing by zero, nil reference and out of memory exceptions. The language provides several code templates to deal with such situations.

Typical exception in ELENA inherits system'Exception base class. A method named raise() is declared and used to raise an exception in the code. There are several exceptions in system library : OutOfRangeException, InvalidArgumentException, InvalidOperationException, MethodNotFoundException, NotSupportedException, AbortException, CriticalException and so on. A typical exception contains the error message and a call stack referring to the moment when the exception was created.

Raising an exception in ELENA is straightforward.

divide(l,r)
{
    if(r == 0)
    {
        InvalidArgumentException.raise()
    }
    else
    {
        ^ l / r
    }
}

To handle it we have to write TRY-CATCH or TRY-FINALLY constructs.

public program()
{
    console.print("Enter the first number:");
    var l := console.readLine().toInt();
    
    console.print("Enter the second number:");
    var r := console.readLine().toInt();

    try
    {
        console.printLine("The result is ", divide(l,r))
    }
    catch(InvalidArgumentException e)
    {
        console.printLine("The second argument cannot be a zero")
    };
}

The output will be:

Enter the first number:2
Enter the second number:0
The second argument cannot be a zero

An anonymous function declared after catch token is invoked if the raised exception matches the function argument list. If we want to handle any standard exception we can use a base exception class:

    catch(Exception e)
    {
        console.printLine("An operation error")
    };

Several exception handlers can be declared inside the nested class:

catch::
{
    function(InvalidArgumentException e)
    {
       console.printLine("The second argument cannot be a zero")
    }
    
    function(Exception e)
    {
        console.printLine("An operation error")
    }
}

In our case both handlers are matched to InvalidArgumentException exception (because InvalidArgumentException is a child of Exception) but the proper exception handler will be raised because it is described early.

If we need to execute the code whenever an exception is raised or the code works correctly we have to use TRY-CATCH-FINALLY construct. The typical use-case is a resource freeing.

import extensions;

public program()
{
    console.printLine("Try");
    try
    {
        var o := new Object();
        o.fail();
    }
    catch(Exception e)
    {
        console.printLine("Error!");
    }
    finally
    {
        console.printLine("Finally");
    }
}

The output will be:

Try
Error!
Finally

If the second line is commented out the output will be:

Try
Finally

If we want to execute the code after the operation whenever an exception is raised, we can skip catch part:

    console.printLine("Try");
    try
    {
        var o := new Object();
        o.fail();
    }
    finally
    {
        console.printLine("Finally");
    }

And the result is:

Try
Finally
system'Object : Method fail[1] not found
Call stack:
sandbox'program.function:#invoke:sandbox.l(9)
system'$private'entry.function:#invoke:app.l(5)
system'$private'entrySymbol#sym:app.l(13)

Symbols

Symbols are named expression which could be use to make the code more readable. A typical symbol is a constant declaration. Though they can more. Symbols are used to declare global singleton instances. The module initialization code is implemented via them. They can be private or public.

Values / Constants

A typical symbol use case is a value which can be reused further in the code:

import extensions;
import extensions'math;

real PiOver2 = RealNumber.Pi / 2;

public program()
{
    console.printLine("sin(",PiOver2,")=", sin(PiOver2))
}

Here we declare a private strong-typed symbol PiOver2. The type can be omitted. In this case it will be a weak typed value. The result is:

sin(1.570796326795)=1.0

If the symbol should be public (be accessible outside the module), public attribute should be put before the type and the name.

If the symbol value can be calculated in compile-time we can declare a constant symbol

import extensions;
import extensions'math;

public const int N = 3;

public program()
{
    for (int i := 0; i < N; i++)
    {
        console.printLine(i,"^2=", sqr(i))
    }
}

The output is:

0^2=0
1^2=1
2^2=4

Static symbols

A normal symbols is evaluated every time it is used. A static symbol is a special case which is initialized only once by the first call (so called lazy loading) and its value is reused for every next call. It is a preferred way to implement a singleton (if it is not stateless).

import extensions;
import extensions'math;

static inputedNumber = console.print("Enter the number:").readLine().toInt();

public program()
{
    console.printLine(inputedNumber,"^3 = ", power(inputedNumber, 3))
}

In this example the symbol is evaluated only once and the entered value is preserved, so the number should be entered only once:

Enter the number:4
4^3 = 64

Preloaded symbols

A preloaded symbol is a special symbol which is automatically evaluated on the program start if namespace members are used in the program. So it can be used for module initialization.

import extensions;

onModuleUse : preloaded = console.printLine("Starting");

public program()
{
    console.printLine("Hello World")
}

The output is:

Starting
Hello World

Types

In programming a data type (or simply type) defines the role of data : a number, a text and so on. In object-oriented this concept was extended with a class which encapsulates the data and operations with them. The data is stored in fields and the operations are done via methods. In ELENA both a type and a class means mostly the same and can be used interchangeably without loss of meaning (except primitive types). Classes form a hierarchy based on inheritance. It means every class (except a super one - system'Object) has a parent and a single one.

A programming language can have static or dynamic type systems. In OOP classes can be made polymorphic fixing part of the problem with static types. Dynamic languages resolve types in run-time. This affects their performance. Modern programming languages as a result of it try to combine these approaches. In ELENA the types are dynamic and are resolved in run-time. In this sense you can write your program without explicitly specifying types at all. But in most cases the types should be specified to make your code more readable and to improve its performance.

In dynamic languages the operation to invoke a method is usually called sending a message. The class reacts to the message by calling appropriate method (with the same name and a signature). In normal case it is done via searching the appropriate handler in the method table. And it takes time. To deal with this ELENA allows to declare sealed or closed classes. Methods of sealed class can be resolved in compile-time and called directly. Methods of closed classes (or interfaces) can be resolved in compile-time via a method table. But the sealed class can not be inherited. Interfaces can be inherited but no new methods can be declared (except private ones).

In ELENA typecasting are implemented via a sending a special methods - conversion handlers. Every class can be converted into another one if it handles this message. Otherwise an exception is raised.

Primitive types

In most cases a class and a type mean the same. The only exception is a primitive type. Primitive types are built-in and support only predefined operations. They are classical data types in sense that they are pure data. The following types are supported by ELENA:

Type Size Description
__float 8 A 64-bit floating-point number
__int 1 A 8-bit integer number
__int 2 A 16-bit integer number
__int 4 A 32-bit integer number
__int 8 A 64-bit integer number
__raw a raw data
__ptr 4 a 32-bit pointer
__mssg 4 a message reference
__mssg 8 an extension reference
__subj 4 a message name reference
__symbol 4 a symbol reference
__string an array

Primitive types can be used in the program with a help of appropriate wrappers. Every time a primitive type is used in the code (except primitive operations) it is boxed into its wrapper. After the operation it is unboxed back. Due to performance issues no validation is applied to the primitive operations, so it is up to a programmer (or a wrapper class) to handle it correctly.

Classes

Putting aside primitive types every program object is an instance of a class. A class is a structure encapsulating data (fields) with operations with them (methods). A class can specify constructors to create an object and conversion routines to convert it from another object.

Declaring and using classes is straightforward for anyone familiar with C-like object-oriented languages:

import extensions;

// declaring a class named A
class A
{
    // declaring a field named a
    field a;
    
    // declaring a default constructor
    constructor()
    {
        a := "Some value"
    }
    
    // declaring a method named printMe
    method printMe()
    {
        console.printLine(a)
    }
}

public program()
{
    // creating an instance of A class
    var o := new A();
    
    // calling a method 
    o.printMe()
}

The output is:

Some value

The keyword class specifies a normal class. A class body is enclosed in curly brackets. A class name declared right before the class body. A field can be declared in any place inside the class body. It can starts with an attribute field. Implicit constructor is named constructor. A method can be declared with a keyword method. The code could be placed inside curly brackets.

The classes form an hierarchy. Every one (except the super one) has a parent. If the parent is not specified the class inherits system'Object (a super class). A child class can override any non-sealed method of its parent. An instance of a child class is simultaneously of a parent type (so called is-a relation). So we could assign a child to a variable of a parent type. But the overridden methods will be used (polymorphic code). In case of weak types (or type-less) this is true as well (we only assume that all variables are of the super type).

import  extensions;

class Parent
{
    field f;
    
    constructor()
    {
        f := "some value"
    }
    
    printMe()
    {
        console.printLine("parent:",f)
    }
}

class Child : Parent
{
    constructor()
    {
        f := "some value"
    }

    printMe()
    {
        console.printLine("child:",f)
    }
}

public program()
{
    Parent p := new Parent();
    Child c := new Child();
    
    p.printMe();
    c.printMe()
}

The output will be:

parent:some value
child:some value

The parent clause should follow the class name and be introduced with a colon. To override the method we have only declare it once again with the same name and signature.

In ELENA a type (or a class) can be used directly in the code like any other symbols. The only difference that we cannot invoke the default constructor. The named constructors should be used instead. NOTE : the default constructor will be called automatically.

import extensions;

// declaring a class named A 
class A
{
    field a;
    
    // default constructor
    constructor()
    {
        a := "a"
    }
    
    // explicit constructor named new
    constructor new()
    {
        // default constructor is called automatically
    }
}

// declaring a class named B
class B
{
    field b;
    
    // default constructor
    constructor()
    {
        b := 2
    }
    
    // explicit constructor named new
    constructor new()
    {
        // default constructor is called automatically
    }
}

// factory function
factory(class)
    // sending a message - name and returning a result of the operation 
    = class.new();

public program()
{
    // creating objects using a class directly
    var a := factory(A);
    var b := factory(B);
    
    // printing the object types
    console
        .printLine(a)
        .printLine(b);
}

The output is:

mylib'$private'A
mylib'$private'B

By default a class is declared private. It means it cannot be accessed outside its namespace. In the case of a library we would like to reuse it. So we have to provide a public attribute:

public class B
{
}

Now the class can be accessed either outside the library or by a reference:

public program()
{
    var b := new mylib'B()
}

A reference (or a full name) consists of a namespace (which in turn can contain sub-namespaces separated by apostrophes) and a proper name separated by an apostrophe.

Abstract classes

An abstract class is a special class used as a basis for creating specific classes that conform to its protocol, or the set of operations it supports. Abstract classes are not instantiated directly. It differs from an interface that the children can extend its functionality (declaring new methods). The abstract class can contain both abstract and normal methods. An abstract method must have an empty body (semicolon)

import extensions;

abstract class Bike
{  
    abstract run(); 
}
  
class Honda4 : Bike
{  
    // overriding an abstract method
    run()
    {
        console.printLine("running safely")
    }
}    

public program()
{
    Bike obj := new Honda4();
    obj.run()
}        

The output will be:

running safely

Classes based on an abstract class must implement all the parent abstract methods. If a child class adds a new abstract methods or implements only some of the parent abstract methods, it should be declared abstract as well.

abstract class BikeWithTrolley : Bike
{
    abstract pack();
}

Interfaces

An interface is a special case of an abstract class. For performance reason a new method cannot be declared for the interface children (except private ones). As a result an interface method can be called semi-directly (via a method table). An interface methods can be both abstract and normal ones (like in abstract classes).

interface IObserver
{
    // declaring an interface abstract method
    abstract notify();
}

Though we could directly inherit the interface:

class Oberver : IObserver
{
    notify() 
    {
        console.printLine("I'm notified")
    }
}

it is unpractical, because we cannot extends the class functionality. Instead we can use a template named interface:

import extensions;

// declaring an interface
interface IObserver
{
    // declaring an interface method to be implemented
    abstract notify();
}

// creating a class supporting an interface
class MyClass : interface<IObserver>
{
    // implementing an interface method
    notify() 
    {
        console.printLine("I'm notified")
    }
    
    // implementing some other functionality
    doSomework()
    {
        console.printLine("I'm doing some work")
    }
}

// simple routine to invoke the interface
sendNotification(IObserver observer)
{
    observer.notify()
}

public program()
{
    // creating an intance of MyClass
    auto myObject := new MyClass();
    
    // do some work
    myObject.doSomework();
    
    // passing the interface implementation to the function by 
    // explicit typecasting the object
    sendNotification(cast IObserver(myObject));    
}        

The output is:

I'm doing some work
I'm notified

The class can implement several interfaces (so we can by-pass a problem with a single inheritance).

import extensions;

// declaring an interface
interface IObserver
{
    // declaring an interface method to be implemented
    abstract notify();
}

// declaring the second interface
interface IWork
{
    abstract doSomework();
}

// creating a class supporting both interfaces
class MyClass : interface<IObserver>, interface<IWork>
{
    // implementing an interface method
    notify() 
    {
        console.printLine("I'm notified")
    }
    
    // implementing the second interface
    doSomework()
    {
        console.printLine("I'm doing some work")
    }
}

// simple routine to invoke the interface
sendNotification(IObserver observer)
{
    observer.notify()
}

public program()
{
    // creating an intance of MyClass 
    auto myObject := new MyClass();
    
    // implicitly typecast it to the second interface
    IWork work := myObject;
    
    // use the second interface
    work.doSomework();
    
    // passing the interface implementation to the function by 
    // explicit typecasting the object
    sendNotification(cast IObserver(myObject));    
}

with the same result:

I'm doing some work
I'm notified

Singletons

Singletons are special classes which cannot be initialized (constructors are not allowed) and only a single instance exists. Singletons are mostly stateless (though they can have static fields). They are always sealed.

The singleton class is declared with singleton attribute. It can be referred directly using a class reference:

import extensions;

// declaring a singleton
public singleton StringHelpers
{
    // Gets the first character of a string.
    char first(string str)
    {
        ^ str[0]
    }
}

public program()
{
    var str := "My string";
    
    // calling a singleton method
    console.printLine("Calling StringHelpers.First(""",str,""")=", 
         StringHelpers.first(str))
}

The output will be:

Calling StringHelpers.First("My string")=M

Structs

Structs are special kind of classes which are stored in-place, rather than by reference in the memory heap. Structs are sealed (meaning they cannot be inherited). They hold mostly small data values (such as numbers, handlers, pointers). All primitive data handlers are structs.

Struct fields can be either primitive or another structures. No reference types are allowed. All fields should be strong typed for that reason.

In most cases the use of structs is quite straightforward.

import extensions;

// declaring a struct
struct Record
{
   // declaring struct fields
   int x;
   int y;
   
   // declaring struct constructor
   constructor(int x, int y)
   {
       // using this prefix to distinguish a class member from the local one
       this x := x;
       this y := y
   }
   
   printMe()
   {
       console.printLine("Record(",x,",",y,")");
   }
}    

public program()
{
    // creating a struct
    auto r := new Record(2, 4);
    
    // invoking struct method
    r.printMe()
}

The result will be:

Record(2,4)

Note that structs are stored in-place. It means that in our example the object was declared in the method stack. Every time it is used as a weak type, it will be boxed and unboxed after the operation. To improve performance we can declare a struct to be a constant one to avoid unboxing operation. For example all numbers are constant structs:

// declaring a constant struct
public const struct IntNumber : IntBaseNumber
{
    // a field is a primitive 32-bit integer type   
    embeddable __int theValue[4];
...

Strings

Strings are special type of classes (both structs and nonstructural classes) which is used to contain the arrays. As a result the class length is variable. No default constructors (without parameters) are allowed.

public const struct String : BaseValue
{
    __string byte[] theArray;

    constructor allocate(int size)
        = new byte[](size + 1);

It can contain only a single field marked as __string one.

Extensions

Extensions are special stateless classes used to declare extension methods. Extension methods allow you to extend the original class functionality without creating a new derived type, recompiling, or otherwise modifying the original type. Every method declared in an extension class is an extension method.

In normal cases extension classes is never used directly (except in mixins). To be used in other modules they should be declared as a public one. To start to use the extension it is enough to declare it in the same namespace as the code where it will be used. If the extension is declared in another module, the module should be included into the code (using import statement).

import extensions;

// declaring an extension
public extension MyExtension
{
    // every member of the extension is an extension method
    printMe()
    {
        console.printLine("I'm printing ", self);
    }
}

public program()
{
    // invoking an extension method for various types
    2.printMe();
    "abc".printMe();
    2.3r.printMe()
}

The result is:

I'm printing 2
I'm printing abc
I'm printing 2.3

Extensions methods are used in ELENA in many places. For example print and printLine are extension variadic methods. If the class already have the method with the same name it will be used instead of extension one. For example if we extend our previous example with a new class containing the method printMe

MyClass
{
    printMe()
    {
        console.printLine("I'm printing myself");
    }
}

public program()
{
    auto o := new MyClass();
    o.printMe();
}

The correct method will be invoked:

I'm printing myself

But if the object is of weak type, the extension will be called:

public program()
{
    var o := new MyClass();
    o.printMe();
}

The output will be:

I'm printing mytest'$private'MyClass

So it is a good practice not to mix the extension and normal method names.

Extensions can be weak one, meaning that they can extends any object (or instances of system'Object class). But we could always specify the exact extension target:

// declaring an extension of MyClass
public extension MyStrongExtension : MyClass
{
    printMe()
    {
        console.printLine("I'm printing MyClass");
    }
}

MyClass
{
}

public program()
{
    auto o := new MyClass();
    o.printMe();
}

The output will be:

I'm printing MyClass

It is possible to have several extension method with the same name as long as the extension targets are not the same.

The extension can be resolved both in compile and run time. The compiler tries to resolve all extensions directly. But if there are several similar extensions it will generate a run-time dispatcher.

// declaring classes
A;
B;

// declaring several strong-typed extensions with the same name
extension AOp : A
{
    // extending an instance of A 
    whoAmI()
    {
        console.printLine("I'm instance of A")
    }
}

extension BOp : B
{
    // extending an instance of B 
    whoAmI()
    {
        console.printLine("I'm instance of B")
    }
}

public program()
{
    // declaring weak-typed variables
    var a := new A();
    var b := new B();

    // resolving an extension in compile-time is not possible
    // so the run-time dispatcher will be used
    a.whoAmI();
    b.whoAmI();
}

The output will be:

I'm instance of A
I'm instance of B

Sealed / Closed Classes

Sealed and closed attributes are used to improve the performance of the operations with the classes. A sealed class cannot be inherited. As a result the compiler can generate direct method calls for this class (of course when the type is known). All structs and singletons are sealed by default. Declaring a sealed class is quite simple:

sealed class MySealedClass
{
}

Closed classes can be inherited but no new methods (private ones or new fields are allowed). All interfaces are closed. When the type is closed, the compiler can use a method table to resolve the method call.

closed class MyBaseClass
{
    // declaring a "virtual" method
    myMethod() {}
}

class MyChileClass : MyBaseClass
{
    // overriding a method
    myMethod() {}
}

Fields

A field is a variable that is declared in a class or struct. Fields are class members and used to hold its state. Fields can be weak (or of system'Object type) or strong ones. Struct fields can be primitive or another structs. They cannot be weak. Both fixed size and dynamic arrays are supported. All fields are private ones. They cannot be accessed outside its class.

Class Fields

A class field should be declared inside the class body. Field and type attributes are optional.

class MyClass
{
    // a fully qualified field declaration
    field int x;
    
    // field attribute my be omitted
    int y;
    
    // type attribute my be omitted
    field z;
    
    // the minimal declaration
    f;
}

The fields can be accessed in the method by their name.

import extensions;

class MyClass
{
    // declaring a field named x
    int x;

    constructor()
    {
        // constructos are typical places to initialize fields
        x := 2
    }

    printX()
    {
        // field is referred by its name
        console.printLine("MyClass.x=",x)
    }    
}    

public program()
{
    var o := new MyClass();
    
    o.printX()
}

The output will be:

MyClass.x=2

If a local variable and a field names are the same we can use this prefix to tell a field apart:

class MyClass
{
    // declaring a field named x
    int x;

    // declaring an argument named x
    constructor(int x)
    {
        // to tell apart a field from a local, this qualifier should be used
        this x := x
    }

    printX()
    {
        // field is directly referred using this prefix
        console.printLine("MyClass.x=",this x)
    }    
}

public program()
{
    var o := new MyClass(3);
    
    o.printX()
}

The output will be:

MyClass.x=3

Field Initializer

Constructors are traditional place to initialize the fields. But in some cases it is more convenient to do it in place, after the declaration.

class Record
{
    // an initialization expression can be a constant
    int x := 0;

    // it can be a result of operations  
    string y := "My" + "String";

    // or class creation  
    z := new Object(); 
}

In this case, we could not declare a constructor at all and use the default one:

public program()
{
    var p := new Record();
}

An initialization expression can be used alone, for example in the derived classes. In this case we have to use this qualifier:

class MySpecialRecord : Record
{
    // we have to use this prefix to initialize already existing fields
    this x := 2; 
} 

And of course we could use it for initializing class fields right in the code:

struct Point
{
    int x := 0;
    int y := 0;
    
    printMe()
    {
        console.printLine("x:",x,",y:",y)
    }
}

public program()
{
    var p := new Point
    {
        this x := 1;
        
        this y := 2; 
    };
    
    p.printMe()
}

And the output is:

x:1,y:2

Readonly fields

Read-only fields are special type of class fields which can be initialized only in class constructors. To declare read-only field we have to use readonly attribute. All fields of constant classes or structures are read-only.

class MyClass
{
    // declaring a readonly field
    readonly int x;
    
    constructor(int x)
    {
        // have to be initialized in the constructor
        this x := x
    }
    
    printMe()
    {
        // can be used in class methods but cannot be changed
        console.printLine("MyClass.x=",x)
    }
}

Static fields

Static fields are class variables which are shared by all class instances. They can be accessed both from normal and static methods. A value changed in one class instances, will be changed for all class instances. Note that static fields are not inherited by the children classes (similar to the traditional static fields in the languages like C#.).

Let's implement a singleton pattern (Note that we declare a dynamic singleton to tell apart from a static singleton):

import extensions;

public class DynamicSingleton
{
    // declare a private constructor to prevent the class 
    // from direct use
    private constructor() {}
    
    callMe()
    {
        console.printLine("I'm a singleton")
    }
    
    // declaring a static field
    static DynamicSingleton instance;
    
    // declaring a static property to access the field
    static DynamicSingleton Instance
    {
        get()
        {
            if (instance == nil)
            {
                instance := new DynamicSingleton();
            };
            
            ^ instance
        }
    }
}

public program()
{
    DynamicSingleton.Instance.callMe();
}

The output will be:

I'm a singleton

Static singleton (declared with singleton attribute) can have static fields as well:

singleton MySingleton
{
    static mySingletonField := "Some value";
    
    printMe()
    {
        console.printLine(mySingletonField)
    }
}

public program()
{
    MySingleton.printMe()
}

And the result should be:

some value

Class Constants

Class constants are constants declared on the level of the class. They can be inherited and accessed in the normal and static methods. Class constants can be initialized only by in-place initializers (in compile-time).

class A
{
   const string nickName := "first";

   printNickname()
   {
      console.printLine(nickName);
   }
}

public program()
{
   var a := new A();
   a.printNickname();
}

There is a special case of the class constants: accumulators. They can be used to provide class meta information. For example we can extend the class meta information in the derived classes:

import extensions;

class Base
{
    // declaring accumulator attribute
    const object[] theFamily;
    
    // adding a reference to the class
    this theFamily += Base;
    
    // declaring inheritable static method (it will be accessible in all derived classes)
    sealed static printFamily()
    {
        console.printLine(theFamily.asEnumerable())
    }
}

class A : Base
{
    // adding a reference to the derived class
    this theFamily += A;
}    

class B : Base
{
    // adding a reference to the derived class
    this theFamily += B;
}    

public program()
{
    console.print("A:");
    A.printFamily();
    
    console.print("B:");
    B.printFamily();
}

The result is:

A:mytest'$private'Base#class,mytest'$private'A#class
B:mytest'$private'Base#class,mytest'$private'B#class

An accumulator attribute of the class A contains the class and its parent. The similar attribute of the class B contains its own copy of the attribute. If a new class will be derived from B, the attribute will be inherited and can be extended further and so on.

Primitive fields

Primitive types are boxed into corresponding wrapper structures. To declare such a structure we have to declare a primitive field:

// declaring an 32bit integer wrapper
struct MyIntNumber
{
    embeddable __int theValue[4];
} 

// declaring an Unicode character wrapper
struct MyCharValue
{
    embeddable __raw theValue[4];
}

// declaring a 64bit floating-point number wrapper
struct MyRealNumber
{
    embeddable __float theValue[8];
}

Fixed size array field

It is possible to declare fixed size array in a data structure. They are useful when you write methods that interop with data sources from other languages or platforms. The array type must be a structure itself. To declare a fixed size array field the field name should be followed by the size constant enclosed in square brackets:

struct WSADATA
{
    short wVersion;
    short wHighVersion;
    // declaring a fixed size arrays of bytes 
    byte  szDescription[257];
    byte  szSystemStatus[129];
}

Dynamic size array field

Arrays are special case of primitive built-in types. The array declaration consists of the element type and empty square brackets ([]). A primitive arrays are encapsulated into a string classes (both reference and structure ones). The string class must have only one field of an array type with __string prefix.

class MyArray
{
    // declaring an array of system'Object  
    __string object[] theArray;

    constructor allocate(int len)
        = new object[](len);
}

struct MyString
{
    // declaring an array of system'CharValue
    __string char[] theArray;
    
    constructor allocate(int len)
        = new char[](len);
}

Constructors

Constructors are special routines to create and initialize objects. ELENA supports several types of constructors: default, conversion and named ones. Constructors can be public, internal, protected or private. If no constructors are declared in a class, default one is automatically created. Constructors are standard way to initialize class fields on object creating. Constructors like normal methods can be overloaded both in run and compile time. Singletons and nested classes cannot have constructors.

Default constructor

A default constructor is an implicit (unnamed) constructor without arguments. A class can have only one default constructor. To declare default constructor we have to do following:

import extensions;

A
{
    // declaring default constructor 
    constructor()
    {
    }
}    

public program()
{
    // create an object using default constructor  
    var a := new A();
    
    console.printLine(a)
}

And the output is:

mylib'$private'A

If no constructors are declared the default one is auto-generated.

import extensions;

// declaring a class without constructors
A;

public program()
{
    // create an object using default constructor  
    var a := new A();
    
    console.printLine(a)
}

If the default constructor is internal, it cannot be called outside the module the class is declared in.

Protected default constructor does not allow to create the class directly. For example the following code cannot be compiled:

A
{
    protected constructor() {}
}

public program()
{
    var a := new A();
}

The compiler will generate an error:

default or conversion constructor is not found

The object still can be created if we provide a public named constructor:

A
{
    // declaring a protected default constructor
    // which cannot be called outside the class
    protected constructor() {}

    // declaring a public named constructor
    constructor new()
    {
    }
}

public program()
{
    // calling a public named constructor
    var a := A.new();
}

Note that the default constructor is automatically called inside named constructors.

The default constructor can be private as well. In this case, class cannot be inherited outside itself:

A
{
    private constructor() {}
}

B : A;

public program()
{
    var b := new B();
}

The code will generate an error:

parent class A cannot be inherited

Unlike sealed classes, ones with private constructors can be inherited internally:

abstract A
{
    abstract printMe();
    
    private constructor() {}
    
    static A Value1 = new A
    {
        printMe()
        {
            console.writeLine("A.Value1")
        }
    };
    
    static A Value2 = new A
    {
        printMe()
        {
            console.writeLine("A.Value2")
        }
    };
}

public program()
{
    var a1 := A.Value1;
    var a2 := A.Value2;
    
    a1.printMe();
    a2.printMe();
}

The output will be:

A.Value1
A.Value2

Note that string classes cannot have default constructor.

Named constructors

Named constructors are constructors with explicitly stated names. In many aspects they are similar to static methods and can be called by sending appropriate messages to class symbols. In contrast to static methods, however, they always return the class instance and automatically call the class default constructor. As a result, a class can have several constructors with the same signature but different names.

A
{
    // declaring a default constructor
    constructor()
    {
        console.writeLine("default constructor")
    }
    
    // declaring a named constructor with an integer argument
    constructor new(int arg)
    {
        console.writeLine("A.new<int>[2]")
    }
    
    // declaring a second named constructor with an integer argument
    constructor load(int arg)
    {
        console.writeLine("A.load<int>[2]")
    }
}

public program()
{
    // creating a class using a default constructor
    var a1 := new A();        
    // creating a class using a named constructor
    // note that default constructor is auto-called  
    var a2 := A.new(1);
    // creating a class using a named constructor
    // note that default constructor is auto-called  
    var a3 := A.load(1);
}

The output will be:

default constructor
default constructor
A.new<int>[2]
default constructor
A.load<int>[2]

If default constructor is not explicitly declared the protected one is auto-generated. As a result it cannot be called outside the class

A
{
    constructor new(int arg)
    {
        console.writeLine("A.new<int>[2]")
    }
}

public program()
{
    // will generate a compile-time error
    var a1 := new A();    
}

We can invoke another named-constructor from a constructor. To do it the resend expression should be declared before the method body. If the method body is an empty it can be omitted.

A
{
    // declaring a default constructor
    constructor()
    {
        console.writeLine("default constructor")
    }     
    
    // declaring a named constructor with an integer argument
    constructor new(int arg)
    {
        console.writeLine("A.new<int>[2]")
    }
    
    // declaring a second named constructor with an integer argument
    constructor load(int arg)
        // declaring a resending expression
        <= new(arg)
    {
        console.writeLine("A.load<int>[2]")
    }
}

public program()
{
    var a := A.load(1);    
}

And the result will be:

default constructor
A.new<int>[2]
A.load<int>[2]

Similar the parent constructor could be called (providing the class does not contain the constructor with the same name and a signature):

Base
{
    constructor()
    {
        console.writeLine("base default constructor")
    } 
    
    // declaring a named constructor with an integer argument
    constructor new(int arg)
    {
        console.writeLine("Base.new<int>[2]")
    }
}

A : Base
{
    // declaring a default constructor
    constructor()
    {
        console.writeLine("A default constructor")
    } 
            
    // declaring a second named constructor with an integer argument
    constructor load(int arg)
        // invoking a parent constructor
        <= super new(arg)
    {
        console.writeLine("A.load<int>[2]")
    }
}

public program()
{
    var a := A.load(1);    
}

The output will be:

A default constructor
Base.new<int>[2]
A.load<int>[2]

If we want to call the parent constructor with the same name we have to declare a redirection to itself:

import extensions;

Parent
{
    constructor new()
    {
        console.printLine("parent constructor")
    }
}

Child : Parent
{
    constructor new()
        // declaring redirection to the parent
        <= super new()
    {
        console.printLine("child constructor")
    }
}

public program()
{
    Child.new();
}

And the output is:

parent constructor
child constructor

Conversion constructors

Conversion constructors are implicit (unnamed) constructors with a strong-typed argument. The compiler will automatically convert the instance of a specified type if there is a conversion constructor with this type:

import extensions;

A;

B
{
    A a;
    
    // declaring a conversion from the type A
    constructor(A a)
    {
        this a := a
    }
}

public program()
{
    A a := new A();
    
    // automatically converting A to B
    B b := a;
}

Note that the conversion constructors do not call the default one. All the class variable initialization should be done here independently.

Multi-constructors

Constructors like normal methods can be dispatched based on the passed arguments both in compile and run-time:

import extensions;

A
{
    constructor new(int arg)
    {
        console.printLine("Passing an integer to named constructor")
    }
    
    constructor new(string arg)
    {
        console.printLine("Passing a string to named constructor")
    }
    
    constructor(int arg)
    {
        console.printLine("Passing an integer to unnamed constructor")
    }
    
    constructor(string arg)
    {
        console.printLine("Passing a string to unnamed constructor")
    }
}

public program()
{
    // compile-time dispatching
    A a1 := A.new(1);
    A a2 := A.new("s");
    
    // run-time dispatching
    var class := A;

    var arg := 1;   
    // both the class and an argument are not known at compile-time 
    var o1 := class.new(arg);
    // and an argument is not known at compile-time 
    o1 := new A(arg);
    arg := "s";    
    // both the class and an argument are not known at compile-time 
    var o2 := class.new(arg);
    // and an argument is not known at compile-time 
    o2 := new A(arg);    
}

And the output is:

Passing an integer to named constructor
Passing a string to named constructor
Passing an integer to named constructor
Passing an integer to unnamed constructor
Passing a string to named constructor
Passing a string to unnamed constructor

Typecasting constructors

In normal case constructors should not have a returning statement. The exception is a typecasting constructor. Typecasting constructor contains only returning expression and the result is converted to the class type. Similar to conversion constructors, the default one is not called.

A
{
    // typecasting constructor 
    constructor load(n)
    // the returning value should convert itself to the expected value   
        = n;
}

If the appropriate conversion constructor exists it will be used:

A
{
    // conversion constructor
    constructor(int n) {}

    // typecasting constructor 
    constructor load(int n)
    // the conversion constructor is called
        = n;
}

Typecasting constructor is the only way to create a string class (one with variable length), because default one is not allowed:

MyArray
{
    // declaring a class with variable length - so called string one 
    __string object[] list;
    
    // typecasting constructor 
    constructor allocate(int n)
    // the result is automatically boxed into the class A
        = new object[](n);
}

public program()
{
    var myArray := MyArray.allocate(3);
} 

Methods

Method is a function declared inside the class scope. It is associated with an instance of the class (similar, a static method / constructor is bound with the class itself). To invoke the method we have to send a message. The process of resolving the method based on the incoming message is called dispatching. If no methods are found an exception is raised. The method which implements the method resolving is called a dispatcher.

A
{
    // declaring a method
    callMe()
    {
        console.writeLine("I'm called")
    }
}

public program()
{
    var a := new A();
    // invoking a method by sending a message
    a.callMe();
}

And the result is:

I'm called

Methods arguments are enclosed in brackets and separated by a comma. If method has no arguments the brackets are empty. The method body is placed after the brackets. In normal cases the code is enclosed inside curly brackets. The statements are separated by a semicolon (except the last one, where the terminator sign is optional).

A
{
    methodWithoutArgs()
    {
       console.writeLine("A method body is placed here")
    }
    
    methodWithWeakArgs(arg1, arg2)
    {
        console.write("methodWithWeakArgs is called with two args:");
        console.writeLine(arg1);
        console.writeLine(arg2);
    }
    
    methodWithStrongTypedArgs(int arg1, string arg2)
    {
    }
}

All methods should return the value. If the return statement is not specified, the method returns itself:

A
{
    myMethod()
    {
        console.writeLine("myMethod is called and return")
        // the returning value is not specified, so the method returns an instance
        // to the current class
    }
}

public program()
{
    var a := new A();
    
    var retVal := a.myMethod();
    console.write(retVal);
}

And the result is:

myMethod is called and return
'$private'A

To return a value from the method we have to use a return operator - ^. It terminates the code flow and returns the following expression. The operator must be the last one in the scope.

A
{
    sum(arg1, arg2)
    {
        // the returning operator returns the result of its expression 
        ^ arg1 + arg2
    }
}

public program()
{
    var a := new A();
    
    var retVal := a.sum(1,2);
    console.write(retVal);
}

The program will print the sum of two numbers:

3 

If the method body contains only the return operator we can simplify our code:

// ...
    sum(arg1, arg2)
        // returning body is placed after = symbol 
        = arg1 + arg2
// ...

We can specify the method returning type. Similar to C-like languages it can be placed before the method name:

// ...
    // declaring a strong-typed method with explicit returning type
    int sum(int arg1, int arg2)
        = arg1 + arg2
// ...

If the returning type is not provided the method result is weak and we can expect any type as a result of the operation.

Method Visibility

By default all methods (as well as constructors / static methods) are public. It means that they are accessible outside the class and its module scope. But in some cases we would like to limit the access by defining the method visibility. ELENA supports private, protected and internal visibility attributes.

Private methods are accessible only inside the class itself. They are used to implement some class-specific functionality. Private methods are resolved in compile-time, so they are relative fast. Moreover the private methods can be declared inside closed classes without any restriction. To declare a private method we have to declare private attribute:

A
{
    // declaring a private method
    private myPrivateMethod()
    { 
        console.writeLine("private method is fired")
    } 

    // declaring a public method
    privateMethodTester()
    {
        // private method is called like a normal one
        self.myPrivateMethod()
    }   
}

public program()
{
    var a := new A();
    a.privateMethodTester() 
}  

And the result is:

private method is fired

If we would like to call it directly, an exception is raised:

public program()
{
    var a := new A();
    a.myPrivateMethod() 
}  

with the following result:

sandbox'$private'A : Method myPrivateMethod[1] not found
Call stack:
sandbox'program.function:#invoke:sandbox.l(20)
system'$private'entry.function:#invoke:app.l(5)
system'$private'entrySymbol#sym:app.l(13)

Private methods are not accessible in the derived classes. If we would like to have a method which is available in all class children we have to declare protected one:

A
{
    // declaring a protected method
    protected myProtectedMethod()
    {
        console.writeLine("A-secific call")
    } 
    
    // declaring a public method to invoke the protected one
    protectedMethodTester()
    {
        self.myProtectedMethod()
    }        
}

B : A
{
    // overridong the protected method
    protected myProtectedMethod()
    {
        console.writeLine("B-secific call")
    }         
}

public program()
{
    var a := new A();
    // calling a protected method from A
    a.protectedMethodTester();
    var b := new B();
    // calling a protected method from B
    b.protectedMethodTester() 
}  

And the result is:

A-secific call
B-secific call

The last visibility attribute is internal. All internal methods are accessible only inside owner module. Usually they are used for operations which are not intended for use outside the module for some reasons, for example unsafe or implementation dependable code.

A
{
    // declaring internal method  
    internal myInternalMethod()
    {
        console.writeLine("Internal method fired")
    } 
}

public program()
{
    auto a := new A();

    // calling internal method    
    a.myInternalMethod()
}

Note that ELENA is a dynamic language, so in normal case any message can be send. That's why to enforce the method visibility we have to deal only with compile-time dispatching. So for example the same code will not work if the variable is weak:

public program()
{
    // declaring a weak-typed variable 
    var a := new A();

    // MethodNotFound exception is raised!    
    a.myInternalMethod()
}

Built-in method variables

There are two built-in method variables in every class method : self and super.

self variable refers to the incoming message target. Note that the message target is not always the same as the method owner (for examples it refers to the group object for mixins).

import extensions;

A
{
    whoAmI()
    {
        // self refers to the message target - 
        // an instance of class A
        console.printLine(self)
    }
}

public program()
{
    var a := new A();
    
    // send a message
    a.whoAmI()
}

And the result is:

'$private'A

We have to use self variable if we want to invoke another method of the same class:

import extensions;

A
{
    whoAmI()
    {
        // self refers to the message target - 
        // an instance of class A
        console.printLine(self)
    }
    
    invokeWhoAmI()
    {
        // invoking the class method  
        self.whoAmI()
    }
}

public program()
{
    var a := new A();
    
    // send a message
    a.invokeWhoAmI()
}

The result is the same:

'$private'A

super built-in variable is used to invoke the parent method:

import extensions;

Base
{
    test()
    {
        console.printLine("Base.test()")
    }
} 

A : Base
{
    test()
    {
        // calling the parent method
        super.test();
        
        console.printLine("A.test()");        
    }
}

public program()
{
    var a := new A();
    
    a.test()
}

And the result is:

Base.test()
A.test()

Method modifiers

Method declaration can be accompanied with modifiers. We already know two method modifier types : visibility (such as public, private, protected and internal) and instancing (e.g. static). Modifiers are used to restrict the method application in one or another way.

Abstract modifier is used to declare an abstract method which should overridden in derived classes. Abstract methods can be declared only inside an abstract class. Non-abstract class cannot have abstract methods. As a result abstract modifier is used to enforce the method overriding

// declaring an abstract class 
abstract class GraphicObject
{
    // declaring an abstract method
    // the abstract method must have an empty body
    abstract draw();
}

class Circle : GraphicObject
{
    // overriding abstract method
    draw() 
    {
        // method implementation
    }
}

Another special case of abstract method is predefined. It is used to declare a method returning type in advanced. For example a super class Object has several predefined method to be correctly used in branching statements:

public class Object
{
    // ...
    predefined bool less(o) {}
    predefined bool greater(o) {}
    predefined bool notless(o) {}
    predefined bool notgreater(o) {}
    // ...
}

Predefined methods can be declared in non-abstract methods. When predefined method is re-declared it should has the same returning type as it was declared.

Sealed modifier is used to prevent the method from overriding. The compiler can use this information to generate more optimized invoke code.

Base
{
    // declaring a sealed method 
    sealed mySealedMethod()
    {
    }
}

A : Base
{
    // an error is raised : method cannot be overriden
    sealed mySealedMethod()
    {
    }
}

Multi-methods

Methods can have the same name but different signatures (argument types). An appropriate method can be resolved in the compile-time (so called method overloading) if all arguments are known. Otherwise it will be resolved in the run-time (so called message dispatching).

import extensions;

A
{
    // declaring two methods with the same name
    // but different signatures
    myMethod(int n)
    {
        console.printLine("myMethod<int>(",n,")")
    }
    
    myMethod(string s)
    {
        console.printLine("myMethod<string>(",s,")")
    }
}  

public program()
{
    auto o := new A();
    // overloading a message in compile-time
    o.myMethod(1);
    o.myMethod("string");
    
    // dispatching a message in run-time
    var v1 := o;
    var v2 := 2;
    var v3 := "string";
    // the argument types are unknown (weak)
    v1.myMethod(v2);
    v1.myMethod(v3);
}

The output in both cases will be the same:

myMethod<int>(1)
myMethod<string>(string)
myMethod<int>(2)
myMethod<string>(string)

If no method matches the arguments an exception is invoked:

import extensions;

A
{
    // declaring two methods with the same name
    // but different signatures
    myMethod(int n)
    {
        console.printLine("myMethod<int>(",n,")")
    }
    
    myMethod(string s)
    {
        console.printLine("myMethod<string>(",s,")")
    }
}  

// declaring a dummy class
B;

public program()
{
    var o := new A();
    // dispatching a message in run-time
    var v := new B();
    o.myMethod(v);
}

The program will generate an error:

sandbox'$private'A : Method myMethod[2] not found
Call stack:
sandbox'program.function:#invoke:sandbox.l(26)
system'$private'entry.function:#invoke:app.l(5)
system'$private'entrySymbol#sym:app.l(13)

We can declare a default message handler which will be called for any non-resolved argument:

import extensions;

A
{
    // declaring two methods with the same name
    // but different signatures
    myMethod(int n)
    {
        console.printLine("myMethod<int>(",n,")")
    }
    
    myMethod(string s)
    {
        console.printLine("myMethod<string>(",s,")")
    }
    
    // default method
    myMethod(o)
    {
        console.printLine("deault myMethod(",o,")")
    }    
}   
    
// declaring a dummy class
B;

public program()
{
    var o := new A();
    // dispatching a message in run-time    
    var v := new B();
    var v2 := 2;
    // default handler will be invoked
    o.myMethod(v);
    // a method with int argument will be invoked
    o.myMethod(v2);
}

The output will be:

deault myMethod('$private'B)
myMethod<int>(2)

Variadic methods

Variadic functions are functions which take a variable number of arguments. For example printLine is a variadic extension method.

import extensions;

public program()
{
    console.print("variadic function ", "can ", "take ");
    console.print("any ", "number ", "of ", "arguments ");
    console.printLine("!")
}

Variadic methods can mix normal and variadic arguments. But the variadic argument should be the last one. The variadic argument starts with params attribute and should be an array type.

import extensions;

singleton Ops
{
    // declaring a variadic method
    // with a normal and a variadic arguments
    sum(int l, params int[] args)
    {
        int sum := l;
        // variadic argument is considered as a special array type
        // supporting basic operations, like Length[1], at[2] or setAt[3]
        int len := args.Length;
        for (int i := 0; i < len; i++)
        {
            sum += args[i]
        };
        
        ^ sum
    }
}

public program()
{
    // variadic argument can be empty
    console.printLine(Ops.sum(1));
    // or contains "any" number of arguments
    console.printLine(Ops.sum(1,2));
    console.printLine(Ops.sum(1,2,3,4,5));
}

The result is:

1
3
15

A variadic argument can be boxed into normal array automatically:

import extensions;

singleton Ops
{
    boxVArg(params object[] args)
    {
        // an variadic argument is boxed into dynamic array
        var o := args;
        
        // print a boxed variadic argument as a collection 
        console.printLine(o.asEnumerable())
    }
}

public program()
{
    Ops.boxVArg(1,2.0r,"abc");
}

The program produces the following result:

1,2.0,abc

In some cases we have to pass a variadic argument further into another variadic function. It can be done with a help the params attribute. Let's summarize and print integers. We will do it by using two variadic function: one to summarize a variadic argument and another to represent it as a string.

import extensions;
    
singleton Ops
{
    concat(params int[] args)
    {
        string ret ;
        for(int i := 0, i < args.Length, i += 1)
        {
            if (ret == nil) {
                ret := ""
            }
            else ret += ",";
            
            ret += args[i].toPrintable()
        };
        
        ^ ret
    }
    
    int sum(params int[] args)
    {
        int sum := 0;
        for(int i := 0, i < args.Length, i += 1)
        {
            sum := sum + args[i]
        };
        
        ^ sum
    }
    
    sumAndPrint(params int[] args)
    {
        // passing variadic argument further
        console.printLine("sum(",self.concat(params args),")=",
            self.sum(params args));
    }
}

public program()
{
    Ops.sumAndPrint(1,2,3,4,5);
}

As expected, the result is:

sum(1,2,3,4,5)=15

Ref Arguments

In ELENA, arguments can be passed either "by value" or "by reference". "By value" parameter cannot be reassigned inside the function, method or a constructor. On the other hand, "by reference" parameters can be changed and these changes will be visible outside the function. As a result, they can be used to return multiple values.

Note that the terms "by value" and "by reference" are misleading. In fact all dynamically allocated objects are passed by reference. So we use them in the sense if they can be reassigned inside the function or not.

To declare a "by reference" argument it must be preceded by ref attribute. Similar this attribute is used to pass the parameter:

import extensions;

// declaring byref arguments
exchange(ref v1, ref v2)
{
    var tmp := v1;
    // byref argument can be changed inside the function
    v1 := v2;
    v2 := tmp
}

public program()
{
    var o1 := 2;
    var o2 := "abc";
    
    console.printLine("before:",o1,",",o2);
    // passing byref parameters
    exchange(ref o1, ref o2);
    console.printLine("after:",o1,",",o2)
}

The result is:

before:2,abc
after:abc,2

"By reference" arguments can be weak (as in the example above) or strong one:

import extensions;

// declaring a strong-typed function 
minMax(ref int min, ref int max, params int[] args)
{
    min := args[0];
    max := args[0];
    for (int i := 1; i < args.Length; i++)
    {    
        if (min > args[i])
        {
            min := args[i]
        };
        if (max < args[i])
        {
            max := args[i]
        }
    }
}

public program()
{
    // we can declare byref arguments just in-place
    minMax(ref int min, ref int max, 1,3,-4,7);
    
    console.printLine("min is ", min);
    console.printLine("max is ", max);
}

The output is:

min is -4
max is 7

Re-sending methods

In some cases the method body contains only a single statement which sends another method to itself. For example we may simulate an optional argument with the code below:

A
{
    trim()
    {
        // the method body contains only another method call
        self.trim(#32)
    }
    
    trim(char ch)
    {
        // ... 
    }
}

In such cases we can simplify our code by using so-called re-sending method body. It is placed after <= operator and contains only a message name with parameters enclosed in brackets. The message can be the same (but of course with different argument list) or another one.

A
{
    // declaring re-sending method
    trim()
        <= trim(#32);
    
    trim(char ch)
    {
       // ...
    }
}

Re-sending methods are especially useful for multi-method default implementation:

A
{
    // declaring a multi-method accepting an integer argument
    do(int arg)
    {
        // ... 
    }
    
    // declaring a multi-method accepting a string argument
    do(string s)
    {
        // ... 
    }
    
    // declaring a default multi-method casting an argument to the integer type
    do(arg)
        <= do(cast int(arg));
} 

Dispatch methods

Resend methods send a message to the same class. In contrast, a dispatch method dispatches an incoming message to another object. For example the following code:

Variable
{
    field value;
    
    constructor(value)
    {
        this value := value
    }
    
    add(v)
    {
        // dispatching the message to another object
        ^ value.add(v)
    }
}

can be simplified using a dispatch method:

Variable
{
    field value;
    
    constructor(value)
    {
        this value := value
    }
    
    // a dispatch method contains only redirect statement
    add(v)
        => value;
}

The dispatch method target is placed after => symbol and can be an expression itself.

Custom dispatcher

A custom dispatcher is a special case of a dispatch method. A class dispatcher is invoked every time when the message handler has to be resolved in compile-time. system'Object contains the default implementation which looks through a method table and invoke the appropriate method or raises an exception of no match is found. It is possible to override the class dispatcher to implement some dynamic features, such as mixins.

A custom dispatcher is a dispatch method named dispatch without arguments and an output type.

Let's implement mixin classes to simulate multiple-inheritance. A mixin is a class that can be used as a part of a special group object, which functionality consists of several not related with each other classes. We will implement a group object with a help of a custom dispatcher:

// declaring a mixin 
singleton CameraFeature
{
    cameraMsg
        = "camera";
}
 
// declaring a mixin 
class MobilePhone
{
    mobileMsg
        = "phone";
}

// declaring a group object
class CameraPhone : MobilePhone
{
    // declaring a custom dispatcher
    dispatch() => CameraFeature;
}
 
public program()
{
   var cp := new CameraPhone();
 
   console.writeLine(cp.cameraMsg);
   console.writeLine(cp.mobileMsg)
}

And the result is:

camera
phone

A custom dispatcher redirects all not resolved by the class messages to its target.

Generic methods

A Generic method is a special type of the methods which accepts any incoming message with matching number of arguments:

import extensions;

A
{
    // declaring a generic method accepting any method with a single argument 
    generic(n)
    {
        console.printLine("generic[2]")
    }
    
    // declaring a generic method accepting any method with two arguments 
    generic(n1,n2)
    {
        console.printLine("generic[3]")
    }
}

public program()
{
    var a := new A();
    a.testMe(2);
    a.andMeTo("abc");
    a.testWithTwoArguments(2,"abc");
}

the result is:

generic[2]
generic[2]
generic[3]

It is possible to identify the incoming message using built-in variable __received:

import extensions;

A
{
    generic(n)
    {
        // __received refers to the current message
        console.printLine("message ",__received," is invoked")
    }
    
    generic(n1,n2)
    {
        console.printLine("message ",__received," is invoked")
    }
}

public program()
{
    var a := new A();
    a.testMe(2);
    a.andMeTo("abc");
    a.testWithTwoArguments(2,"abc");
}

The program result is:

message testMe[2] is invoked
message andMeTo[2] is invoked
message testWithTwoArguments[3] is invoked

Properties

In ELENA class fields cannot be directly accessed outside their classes. So we have to provide special means to work with them - properties.

Properties are special sort of class members used to mimic field-like syntax. Though the operations with properties look like direct ones (similar to C), "under the hood" special access methods are used : get and set.

import extensions;

public program()
{
    var s := "MyString";
    // accessing a class property is quite straight-forward
    console.printLine("s.Length=",s.Length);
}

The output is:

s.Length=8

In simplest case the property declaration is quite straight-forward and consists of only a get-method (so-called read-only property):

import extensions;

A
{
    // declaring a field 
    x := 2;
    
    // declaring a read-only property consisting of get-method 
    X = x;
}

public program()
{
    var a := new A();
    
    console.printLine("a.X=",a.X);
}

The result is:

a.X=2

In case the get-method requires several code lines, the full get-method declaration should be used:

import extensions;

A
{
    // declaring a field 
    x := 2;
    
    // a normal get-method should start with get attribute
    get RealX()
    {
        var r := x.toReal();
        
        ^ r
    }
}

public program()
{
    var a := new A();
    
    console.printLine("a.RealX=",a.RealX);
}

with the following result:

a.RealX=2.0

If we need to set a property value, set-method should be declared as well:

import extensions;

A
{
    // declaring a field 
    x := 2;
    
    // declaring a simple get-method
    X = x;
    
    // declaring a set-method
    set X(val)
    {
        x := val
    }
}

public program()
{
    var a := new A();
    
    // setting a property value
    a.X := 3;
    
    console.printLine("a.X=",a.X);
}

The result is:

a.X=3

Another way to declare a property is to use so-called explicit property declaration:

A
{
    // declaring a field 
    int x := 2;
    
    // declaring an explicit property
    int X
    {
        // get-method
        get()
            = x;
            
        // set-method
        set(int x)
        {
            this x := x
        }
    }
}

The property can be either weak or strong-typed one (as in the example above).

We can use prop property templates to simplify our code:

import extensions;

A
{
    // declaring a property using prop template
    object X:prop;
    
    printMe()
    {
        // we can use the field directly
        console.printLine("A.X=",X)
    }
}

public program()
{
    // outside the class the appropriate property can be used
    var a := new A();
    a.X := 2;
    
    a.printMe()
}

The result is:

A.X=2

Both weak and strong-typed properties are supported

A
{
    // declaring a strong-typed property 
    prop int X;
// ... 

Nested classes

Developer Documentation

Producing a code in run-time

Evaluation tape

In general we could generate a code in run-time either by just-in-time compilation into some temporal binaries or interpreting.

Instead tree-like data structures can be used to build our code as an expression tree.

The created expression tree should be somehow executed. Here again we could use an reflection or run-time compilation. Alternatively the expression tree can be turned into an evaluation tape containing an array of functions.

The logic of an evaluation tape is quite simple. It executes every tape function with a reference to the process stack as a variadic argument. The result of the function is put into the stack. If the function has fixed arguments they are removed from the stack. The process stops when the last function is executed and the object at the stack top is returned as a process result. The current tape element index is placed into the stack as well, so it is possible to implement branching and looping.

Let's execute the following code:

console.writeLine("Hello")

Which can be declared as a following tree:

new system'dynamic'expressions'MessageCallExpression ( 
   new system'dynamic'expressions'SymbolExpression ( 
      "console"
   ), 
   "writeLine",
   new system'dynamic'expressions'ConstantExpression ( 
     "Hello"
   ) 
) 

The tree can be turned into the following evaluation tape:

new FunctionTape (
   new ConstantFunction("Hello"),
   new ConstantFunction(console),
   new MessageFunction("writeLine[2]"),
   new ReleaseFunction()
)

After the execution the first two functions the process stack contains : { console, "Hello" }. The next function will send a message "writeLine[2]" to the element at the stack top - console. After the operation two elements will be removed from the process stack and the message result will be placed back: { console }. The last function will remove a top element from the stack.

So it is quite easy. The evaluation tree can be easily generated and relatively fast executed.

ELENA Byte Codes

ELENA Virtual Machine Registers

Mnemonic Description
f(p) frame pointer (positive values - pointing to the current frame, negative - to the previous frame)
s(p) stack pointer (only positive)
a(cc) accumulator (ebx)
index data accumulator (edx)

Register operations

Code Mnemonic Description
91h getai i acc <= acc[i]
94h peekfi i [fp+i] => acc
95h peeksi i [sp+i] => acc;
9Ch setf i fp+i => acc
C0h setai i [sp] => acc[i]
9Fh setm m m => index
9Eh setr r r => acc
BBh savesi i [sp:i] <= index
C4h storefi i [fp+i] <= acc
C3h storesi i [sp+i] <= acc
Clone this wiki locally