-
-
Notifications
You must be signed in to change notification settings - Fork 26
ELENA Programming Manual
- Overview
- Language Environment
- Supported CPUs
- Supported platforms
- Installing
- Language Elements
- Developer Documentation
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)
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 |
Currently x86, x86-64, Aarch64 and PPC64le are supported.
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
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
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
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.
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.
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!
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 are one of the most essential data types of a programming language. ELENA supports both integers and floating-point numbers. These are:
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 |
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.
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;
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
ELENA provides a basic set of operations with primitives. It is easily extended with a help of methods declared in appropriate classes and extensions.
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 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
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.
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
Operators are ordered in the following manner, from highest precedence to lowest:
Category | Operators |
---|---|
Multiplication | *, /, $shl, $shr |
Addition | +, - |
Comparisons | ==, !=, <, >, <=, >= |
Bitwise | &&, ||, ^^ |
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)
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 |
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.
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:
♥
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);
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
т
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 |
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
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
^ 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.
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);
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
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 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.
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
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
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
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.
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
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
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");
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 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 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.
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
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
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
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.
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.
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.
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();
}
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 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 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 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 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 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() {}
}
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.
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
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
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 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 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 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];
}
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];
}
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 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.
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 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 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.
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
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);
}
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.
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()
}
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 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()
{
}
}
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 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
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
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));
}
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.
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.
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
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;
// ...
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.
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) |
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 |