Skip to content

Getting started with ELENA Programming Language

Aleksey Rakov edited this page Sep 25, 2019 · 1 revision

Content

Introduction

ELENA is a general-purpose, object-oriented, polymorphic language with late binding. It features message dispatching/manipulation, dynamic object mutation, a script engine / interpreter and group object support.

The simplest program

To create a simple console program we have to declare the public program closure in the project root namespace:

public program()
{
}

Everything in ELENA is an object. To interact with it we have to send a message. The message consists of an action and a parameter list.

The statement should be terminated by a semicolon.

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

In our example the action is writeLine and the parameter list consists of a single string constant. The message target is console object (implementing input / output operations with a program console).

Several message operations can be done in a single statement:

public program()
{
    console.writeLine("Hello!").writeLine("How are you?");
}

The result will be:

 Hello!
 How are you?

We may read a user input by sending readLine message without parameters:

public program()
{
    console.write("What is your name:").writeLine("Hello " + console.readLine())
}

The result will be:

What is your name:Alex
Hello Alex

Console::write method is similar to writeLine except that it writes to the output screen without a new line character.

Declaring a variable

A variable can be declared in an assignment statement starting with var attribute:

var myVariable := "A text";

where we declare a variable myVariable and initialize it with a string constant value.

The assigning value can be an expression itself:

public program()
{
    console.writeLine("Hello!").writeLine("How are you?");
    var s := console.readLine()
}

ELENA is a dynamic language and in most cases we may not specify the variable type:

public program()
{
    var s := "Hello";
    console.writeLine(s);

    s := 2;
    console.writeLine(s);
}

The output will be:

Hello
2

But it is possible to specify the variable expected type:

String s := "Hello";
console.writeLine(s);

where system'String is a class representing text as a sequence of UTF-8 characters.

We may use a class alias to simplify the code:

string s := "Hello";  // string is a String class alias
console.writeLine(s);

ELENA does not enforce types in compilation-time, so the following code will be successfully compiled:

string s := "Hello";
s := 2;

But it will raise an exception in the run-time:

system'IntNumber : Method #cast[0] not found
Call stack:
system'Exception#class.new[1]:exceptions.l(125)
system'MethodNotFoundException#class.new[2]:exceptions.l(236)
system'$inlineC.start[0]:win32_app.l(313)
mytest'program.#invoke[0]:test.l(5)
system'$inlineC.start[0]:win32_app.l(39)
system'#startUp:win32_app.l(52)

As you may see, the compiler injects the typecasting code, so the actual code looks like this:

string s := "Hello";
s := cast string(2);

where cast string(2) is a construction to typecast the target expression - a numeric constant - to the expected type - System'String

As a result if the object supports casting method the operation will work. For example, system'IntNumber can be implicitly converted into system'RealNumber so the following code:

public program()
{
    real r := 2;
    console.writeLine(r)
}

will be successfully executed with the result:

2.0

Basic Types

The Boolean Type

Boolean type is used in conditional operations and may accept only two Boolean literals - true and false.

import extensions;

public program()
{
    bool b1 := true;
    bool b2 := false;
    
    console.printLine(b1,"==",b1," is ",b1==b1);
    console.printLine(b2,"==",b2," is ",b2==b2);
    console.printLine(b1,"==",b2," is ",b1==b2);
    console.printLine(b2,"==",b1," is ",b1==b2);
}

Note that implicit extension method - extensions'outputOp.printLine[] - was used to simplify the output operations.

The output is:

true==true is true
false==false is true
true==false is false
false==true is false

The Numeric types

The most used numeric types in ELENA are 32-bit signed integer number (represented by IntNumber), 64-bit signed integer number (represented by LongNumber) and 64-bit floating-point number (represented by RealNumber):

import extensions;

public program()
{
    int  n := -234;
    long l := 1235456765l;
    real r := 2.3456r;
    
    console.printLine("Integer number - ",n);
    console.printLine("Long integer number - ",l);
    console.printLine("Real number - ",r)
}

The output is:

Integer number - -234
Long integer number - 1235456765
Real number - 2.3456

The String Type

String is used to store the text encoded in UTF-8. String is a read-only collection of CharValue classes each representing UTF-32 symbol. Note that one character may be encoded with more than one byte!.

import extensions;

public program()
{
    var s := "Hello";
    
    console.printLine("The first character of ",s," is ", s[0]);
    console.printLine("The last character of ",s," is ", s[s.Length - 1])
}

The output is:

The first character of Hello is H
The last character of Hello is o

The same code for example with a Russian text will not work. Because every character is encoded with a two bytes and this should be taken into account.

import extensions;

public program()
{
    var s := "Привет";
    
    console.printLine("The first character of ",s," is ", s[0]);
    console.printLine("The last character of ",s," is ", s[s.Length - 1])
}

The output is:

The first character of Привет is П
An index is out of range
Call stack:
system'Exception#class.new[1]:exceptions.l(125)
system'OutOfRangeException#class.new[1]:exceptions.l(156)
system'OutOfRangeException#class.new[0]:exceptions.l(156)
system'String.at[1]:memory.l(1243)
mytest'program.#invoke[0]:test.l(8)
system'$inlineC.start[0]:win32_app.l(39)
system'#startUp:win32_app.l(52)

We may use another class representing UTF-16 text (WideString) to solve this problem:

import extensions;

public program()
{
    var s := "Привет"w. // UTF-16 string
    
    console.printLine("The first character of ",s," is ", s[0]);
    console.printLine("The last character of ",s," is ", s[s.Length - 1]);
}

The output will be correct this time:

The first character of Привет is П
The last character of Привет is т

But this code will not work with Chinese text or any other requiring more than 2 bytes per symbol. So instead we may use enumerators:

import system'routines;
import extensions;

public program()
{
    var s := "Привет";
    
    console.printLine("The first character of ",s," is ", s.FirstMember);
    console.printLine("The last character of ",s," is ", s.LastMember)
}

The output will be correct for any UTF-8 text:

The first character of Привет is П
The last character of Привет is т

Array types

It is possible to declare a generic (system'Array) or strong-typed template-based (system'Array#1) array.

import extensions;

public program()
{
    var strongTypedArray := new int[] {1,2,3};
    
    var genericArray := Array.allocate(3);
    genericArray[0] := 1;
    genericArray[1] := "b";
    genericArray[2] := 2.3r;

    console.printLine("strong-typed array ",strongTypedArray.asEnumerable());
    console.printLine("dynamic-typed array ",genericArray.asEnumerable());
}

The output is:

strong-typed array 1,2,3
dynamic-typed array 1,b,2.3

Basic arithmetic operations

ELENA supports basic arithmetic operations with integer and floating-point numbers:

import extensions;

public program()
{
    var n1 := 12;
    var n2 := 5;
    var n3 := -3;
    var r1 := 2.3r;
 
    console.printLine(n1, " + ", n2, " = ", n1 + n2);
    console.printLine(n1, " - ", n2, " = ", n1 - n2);
    console.printLine(n1, " * ", n3, " = ", n1 * n3);
    console.printLine(n1, " / ", n2, " = ", n1 / n2);

    console.printLine(n1, " + ", n2, " * ", r1 ," = ", n1 + n2 * r1)
}

The result is:

12 + 5 = 17
12 - 5 = 7
12 * -3 = -36
12 / 5 = 2
12 + 5 * 2.3 = 23.5

?? operator

Operator ?? is used to deal with nil.

a ?? b - will return a if a is not nil or b

See the following code:

import extensions;

public program()
{
    var a := nil;
    var b := a ?? 0 + 2;
    console.printLine(a ?? "nil", " ?? 0 + 2 = ", b);
            
    a := 1;
    b := a ?? 0 + 2;
    console.printLine(a ?? "nil", " ?? 0 + 2 = ", b);
}

The output is:

nil ?? 0 + 2 = 2
1 ?? 0 + 2 = 3

The operator can be used for typecasting operations as well:

cast type(a) ?? b - will typecast a to type or return b if it is not possible.

See the code:

import extensions;

public program()
{
    string s := "a";
    var n := 2;
    
    console.printLine("cast int(",s,") ?? 0 = ", cast int(s) ?? 0);
    console.printLine("cast int(",n,") ?? 0 = ", cast int(n) ?? 0);
}

The output is:

cast int(a) ?? 0 = 0
cast int(2) ?? 0 = 2

Conditions, Multi-select, Loops

Conditional statement in ELENA are defined as follows:

if(<Boolean expression>)
{ 
   /* doSomething if TRUE*/ 
}
else
{
   /*doSomehting if ELSE*/
};

We could omit else part

if(<Boolean expression>)
   { /*doSomehting if TRUE*/ };

Usually Boolean expression is a result of a comparison operation:

public program()
{
    console.writeLine("Hello!").writeLine("How are you?");
    var s := console.readLine();
    if(s == "good")
    { 
        console.writeLine("Me too") 
    }
    else
    {
        console.writeLine("What happends?")
    }
}

Several conditions can be checked:

public program()
{
    console.writeLine("Hello!").writeLine("How are you?");
    var s := console.readLine();
    if(s == "good" || s == "fine")
    { 
        console.writeLine("Me too") 
    }
    else
    {
        console.writeLine("What happends?")
    }
}

A switch statement can be implemented using => operator:

public program()
{
    console.writeLine("Hello!").writeLine("How are you?");
    var s := console.readLine();
    s =>
      "good"  { console.writeLine("Me too") }
      "fine"  { console.writeLine("Glad to hear") }
      "bad"   { console.writeLine("What's wrong?") }
      "so so" { console.writeLine("It happens") }
      :       { console.writeLine("What happens?") };
}

We could declare while loop which will be repeated until the condition is true:

public program()
{
    console.writeLine("Hello!").writeLine("Guess what?");
    var s := console.readLine();
    while (s != "nothing")
    {
        console.writeLine("Guess what?");
        s := console.readLine();
    }
}

Alternatively until loop is executed until the condition is met :

public program()
{
    console.writeLine("Hello!").writeLine("Guess what?");
    var s := console.readLine();
    until (s == "nothing")
    {
        console.writeLine("Guess what?");
        s := console.readLine();
    }
}

ELENA supports C-styled for loop as well:

public program()
{
    for(int n := 0, n < 5, n += 1)
    {
        console.write("*")
    }            
}

The output is:

*****

Note that comma is used instead of semicolon!

doUntil loop is similar to for, but the loop is executed at least once:

var text := new StringWriter();
doUntil(string line, line.isEmpty(), line := console.readLine())
{
    text.writeLine(line)
};

Classes, Fields Methods, Constructors

Everything in ELENA is a class. So to implement some tasks we will have to declare our own classes.

Declaring a simple class

Let's create a simple class :

import extensions;

class MyClass
{
    // a field
    string myString;

    // an implicit constructor
    constructor(string s)
    {
        myString := s
    }
    
    // an explicit constructor
    constructor fromNumber(int n)
    {
        myString := n.toString();
    }
    
    // a method
    printString()
    {
        console.printLine(myString)
    }
}

public program()
{
    // creating a class instance by sending new message to the class
    var myClass := new MyClass("This is printed by my class.");
    
    myClass.printString()
}

The output will be:

This is printed by my class.

Note that in ELENA a class is an object itself and can be used by like any other object

Class Inheritance

We may inherit our class. When the parent is not explicitly declared - the class inherits system'Object super class

import extensions;

class MyParent
{
    constructor new()
    {
        console.printLine("Parent Constructor.")
    }

    printMe()
    {
        console.printLine("I'm a Parent Class.")
    }    
}

class MyChild : MyParent
{
    
    constructor new()
        <= new() // calling the parent constructor
    {
        console.printLine("Child Constructor.")
    }
    
    printMe()
    {
        // calling the parent method
        super.printMe();
        
        console.printLine("I'm a Child Class.")
    }
}

public program()
{
    var myClass := MyChild.new();
    
    myClass.printMe()
}

The output is:

Parent Constructor.
Child Constructor.
I'm a Parent Class.
I'm a Child Class.

Private methods

It is possible to declare the private methods which cannot be called outside the class.

import extensions;

class MyClass
{
    private printPrivate()
    {
        console.printLine("private print.")
    }
    
    printPublic()
    {
        console.print("Calling from public print - ");

        // self is a reference to the current object
        self.printPrivate()
    }
}

public program()
{
    // Note that if the constructor explicitly is not declared 
    // the system'Object one (without input parameters) is inherited
    var myClass := new MyClass();
    
    myClass.printPublic();
    myClass.printPrivate()
}

The output is:

Calling from public print - private print.
mytest'$private'MyClass : Method printPrivate[0] not found
Call stack:
system'Exception#class.new[1]:exceptions.l(125)
system'MethodNotFoundException#class.new[2]:exceptions.l(236)
system'$inline16.start[0]:win32_app.l(313)
mytest'program.#invoke[0]:test.l(26)
system'$inline16.start[0]:win32_app.l(39)
system'#startUp:win32_app.l(52)

Properties

In normal case the class fields cannot be accessed outside the class. That's why we may declare special methods to access it:

import extensions;

class MyClass
{
    int _x;

    get int x() = _x;  // get accessor

    set x(int o)       // set accessor 
    {
       _x := o
    }
}

public program()
{
    var myClass := new MyClass();

    myClass.x := 2;

    console.printLine("MyClass.x=", myClass.x)
}

The output is:

MyClass.x=2

We may use property scope as well:

import extensions;

class MyClass
{
    int _x;
    
    x
    {
        int get = _x;
        
        set(int val)
        {
            _x := val;
        }
    }
}

public program()
{
    var myClass := new MyClass();

    myClass.x := 2;

    console.printLine("MyClass.x=", myClass.x)
} 

Simple accessors can be omitted by using prop field template:

import extensions;

class MyClass
{
    prop int x;
}

public program()
{
    var myClass := new MyClass();

    myClass.x := 2;

    console.printLine("MyClass.x=", myClass.x)
}

Exception Handling

We may use try-catch statement to handle the possible exceptions:

import extensions;

public program()
{
    try
    {
        new Object().nonExistingMethod();
    }
    catch(MethodNotFoundException e)
    {
        console.printLine("Method not found")
    }
    catch(Exception e)
    {
        console.printLine("Unknown error")
    }
}

The output is :

Method not found

Basic Object Interactions

The main way to interact with objects in ELENA is message sending. The language supports general and strongly typed methods with several parameters. It is possible to declare multi and variadic methods.

Let's start with a simple example:

import extensions;

class MyClass
{
    foo(param)
    {
        console.printLine("calling foo(",param,")")
    }
}

public program()
{
    new MyClass().foo("Hello")
}

The output will be:

calling foo(Hello)

In this code we declare a class - MyClass with a single method - foo. The foo method is a general (i.e. having a signature without explicit types) and has a single parameter - param.

In the main program we create a class instance (by using implicit constructor) and call a method.

General methods may have several parameters or none:

import extensions;

class MyClass
{
    foo()
    {
        console.printLine("calling foo()")
    }

    foo(param1, param2)
    {
        console.printLine("calling foo(",param1,",",param2,")")
    }
}

public program()
{
    new MyClass().foo().foo("Hello","World")
}

The output will be:

calling foo()
calling foo(Hello,World)

If our method should return a value we may use a special operator(similar to Smalltalk): ^ operator is used to terminate execution of the method and returns the following expression result to the calling method or symbol.

import extensions;

class MyClass
{
    foo(param)
    {
        console.printLine("calling foo(",param,")")
    }
        
    sum(n, m)
    {
        console.printLine("calling sum(",n,",",m,")");
    
        ^ n + m
    }
}

public program()
{
    var o := new MyClass();
    o.foo(o.sum(1,2))
}

The output is:

calling sum(1,2)
calling foo(3)

ELENA methods always return something, so if the returning operator is skipped the compiler add the code which returns an instance of the called object. E.g.:

    foo(param)
    {
        console.printLine("calling foo(",param,")");

        ^ self // auto-compiled
    }

where built-in variable self refers to the instance of the method class.

If our method contains only returning operator, we may simplify the code using = operator:

class MyClass
{
    // <...>

    sum(n, m)
        = n + m;
}

The general methods accept any parameter. In case we do need to specify the parameter type, the strong typed methods can be used:

import extensions;

class MyClass
{
    foo(String s)
    {
        console.printLine("calling foo(string:",s,")")
    }
}

public program()
{
    new MyClass().foo("Hello")
}

The output:

calling foo(string:Hello).

String is a class implementing UTF8 encoded literal string. Instead of the class name we may use a type alias - string

   foo(string s)
   {
      // <...>
   }

If we will attempt to pass an instance of another type the exception will be raised. E.g. the code:

// <...>
new MyClass().foo(2);

will raise the following exception:

mytest'$private'MyClass : Method foo[1] not found
Call stack:
system'Exception#class.new[1]:exceptions.l(128)
system'MethodNotFoundException#class.new[2]:exceptions.l(239)
system'$inline16.start[0]:win32_app.l(318)
mytest'program.#invoke[0]:test.l(13)
system'$inline16.start[0]:win32_app.l(39)
system'#startUp:win32_app.l(52)

We may always declare the method handler for supported parameter types:

import extensions;

class MyClass
{
    foo(string s)
    {
        console.printLine("calling foo(string:",s,")")
    }
    
    foo(int n)
    {
        console.printLine("calling foo(int:",n,")")
    }
}

public program()
{
    var o := new MyClass();
    o.foo("Hello");
    o.foo(2)
}

The result will be:

calling foo(string:Hello)
calling foo(int:2)

Of course it is not practical to declare handlers for all possible parameter types. So instead a default handler may be declared:

import extensions;

class MyClass
{
    foo(string s)
    {
        console.printLine("calling foo(string:",s,")")
    }
    
    foo(int n)
    {
        console.printLine("calling foo(int:",n,")")
    }
    
    foo(o)
    {
        console.printLine("foo : unsupported parameter:",o)
    }
}

public program()
{
    var o := new MyClass();
    o.foo("Hello");
    o.foo(2);
    o.foo(3l)
}

The output:

calling foo(string:Hello)
calling foo(int:2)
foo : unsupported parameter:3

Using default multi-method handler is more efficient than handling an exception. For example the handler may be used to explicitly convert the parameter to the expected type:

import extensions;

class MyClass
{
    foo(string s)
    {
        console.printLine("calling foo(string:",s,")")
    }
    
    foo(int n)
    {
        console.printLine("calling foo(int:",n,")")
    }
    
    foo(o)
        <= foo(o.Printable);
}

public program()
{
    var o := new MyClass();
    o.foo("Hello");
    o.foo(2);
    o.foo(3l)
}

The output will be:

calling foo(string:Hello)
calling foo(int:2)
calling foo(string:3)

Note that for multi method a redirect shortcut code was used:

        <= foo(o.Printable);

is equivalent to:

        = self.foo(o.Printable);

In normal case the method may return any object (alternatively we may say that it returns an instance of the super class - system'Object). But is is possible to explicitly specify the type of returning value:

import extensions;

class MyClass
{
    string toClassString()
        = "instance MyClass";
        
    foo(string s)
    {
        console.printLine("calling foo(string:",s,")")
    }
    
    foo(int n)
    {
        console.printLine("calling foo(int:",n,")")
    }
    
    foo(o)
        <= foo(o.Printable);
}

public program()
{
    var o := new MyClass();
    o.foo(o.toClassString());
    o.foo("Hello");
    o.foo(2);
    o.foo(3l)
}

The output:

calling foo(string:instance MyClass)
calling foo(string:Hello)
calling foo(int:2)
calling foo(string:3)

Of course the method with parameters may have type prefix as well:

    MyClass foo(string s)
    {
        console.printLine("calling foo(string:",s,")")
    }

And finally let's see how we may declare a variadic (i.e. method accepting any number of parameters) method:

import extensions;

class MyClass
{
    foo(string s)
    {
        console.printLine("calling foo(string:",s,")")
    }
    
    foo(int n)
    {
        console.printLine("calling foo(int:",n,")")
    }
    
    foo(o)
        <= foo(o.Printable);
        
    foo(params object[] args)
    {
        for(int i := 0, i < args.Length, i += 1)
        {
            self.foo(args[i])
        }
    }
}

public program()
{
    var o := new MyClass();
    o.foo("Hello",2,3l)
}

The output will be:

calling foo(string:Hello)
calling foo(int:2)
calling foo(string:3)

Working with enumerable objects

ELENA supports several different types of enumerable objects : arrays, lists, dictionaries, ranges and so on.

To be used as enumerable one, object should handle enumerator message.

Let's consider a simple example:

import extensions;

public program()
{
    var list := new object[]{1,2.3r,"String",3l};
    
    var it := list.enumerator();
    while (it.next())
    {
        console.printLine(it.get())
    }
}

In the output every member of collection will be printed:

1
2.3
String
3

In this code we declare an array - list. We send enumerator message to it and assign the returned enumerator to the variable it. Then we repeat the code until next message returns false value. get message returns a current enumeration member.

The same pattern can be applied for any enumerable object:

import extensions;
import system'routines;

public program()
{
    var range := new Range(1, 10);
    
    var it := range.enumerator();
    while (it.next())
    {
        console.printLine(it.get())
    }
}

Here we generate and print a range of natural numbers from 1 till 10. The result is:

1
2
3
4
5
6
7
8
9
10

Similar to C# there are a lot of extension methods which can be used for searching, counting, filtering and so on.

Let's start with a simple one - forEach - executes a code for each enumeration member. The code above can be rewritten using forEach extension method (declared in system'routines module) and a closure:

import extensions;
import system'routines;

public program()
{
   new Range(1, 10).forEach:(item){ console.printLine(item) }
}

where (item){ <...> } is a general closure with a single parameter.

Note : if the message has only one single parameter, we may use a colon instead of brackets to pass it

We may further simplify our code using existing closure extensions'routines'printingLn

import extensions;
import system'routines;
import extensions'routines;

public program()
{        
    new object[]{1,2.3r,"String",3l}.forEach:printingLn
}

In both cases the output will be similar to our first two examples.

We may combine several extension methods in a row. For example filterBy is used to filter an enumeration based on the parameter output. Only the members for which filter function returns true will be passed further. It may be used in combination with other extension methods like forEach.

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

public program()
{
    new Range(1, 10).filterBy:(i => i.mod:2 == 0).forEach:printingLn
}

filterBy will return only even numbers. The output will be:

2
4
6
8
10

Note that (i => <...> ) is a lambda closure which is shortcut form of (i){ ^<...> }

summarize extension is used to summarize all the members of the collection:

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

public program()
{
    console.printLine(new Range(1, 10).filterBy:(i => i.mod:2 == 0).summarize())
}

The result will be a sum of first 10 even natural numbers:

30

Using toArray extension we may save our enumeration as an array:

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

public program()
{
    var evens := new Range(1, 10).filterBy:(i => i.mod:2 == 0).toArray();

    console.printLine("sum(",evens,")=", evens.summarize())
}

toArray will collect the enumeration member into the array. And the output is:

sum(2,4,6,8,10)=30

We may limit our output using top extension:

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

public program()
{
    new Range(1,100).filterBy:(i => i.mod:2 == 0).top:10.forEach:printingLn
}

The result will be first 10 even numbers:

2
4
6
8
10
12
14
16
18
20

We may combine several enumerable objects into a single collection using zipBy extension:

import extensions;
import system'routines;
import extensions'routines;

symbol list = new string[] {"a","b","c","d"};

public program()
{
    list
       .zipBy(new Range(1, list.Length), (ch,i => i.Printable + " - " + ch.Printable))
       .forEach:printingLn
}

And the output is:

1 - a
2 - b
3 - c
4 - d

where (ch,i=><...>) is a lambda closure with two parameters

selectBy extension can be used to generate a new collection based on previous one. orderBy extension will sort a collection:

import extensions;
import system'routines;
import extensions'routines;

public program()
{
    var list := new Range(1,5).selectBy:(n => randomGenerator.nextInt(100)).toArray();
    
    console.printLine("sort(",list,")=",list.orderBy:(p,n => p < n))
}

The result will be a randomly generated list of numbers, ordered in assenting order:

sort(50,94,40,78,93)=40,50,78,93,94

groupBy may be used to group the enumeration members into a sub collections:

import extensions;
import system'routines;
import extensions'routines;

public program()
{
    var list := new Range(1,20).selectBy:(n => randomGenerator.nextInt(10)).toArray();
    
    list
        .groupBy:(x => x)
        .selectBy:(sub_list => 
                      sub_list.Key.Printable + ":" 
                      + sub_list.countMembers().Printable + " times")
        .orderBy:ifOrdered
        .forEach:printingLn
}

The code will count how many times a random number is encountered:

0:2 times
1:4 times
2:6 times
3:2 times
4:2 times
5:4 times
6:10 times
7:6 times
8:2 times
9:2 times

Working with dynamic code

Let's start with a multi-methods. We may declare a several methods with the same name but with different signatures. It is also possible to declare an explicit multi-method dispatcher.

class MyClass
{
    // accepts the integer
    testMe(int n)
    {
        console.writeLine:"It is a number"
    }

    // accepts the string
    testMe(string s)
    {
        console.writeLine:"It is a string"
    }

    // default handler
    testMe(o)
    {
        console.writeLine:"Unsupported parameter"
    }
}

public program()
{
    object o := new MyClass();

    o.testMe(2);
    o.testMe("s");
    o.testMe(3l)
}

The output is:

It is a number
It is a string
Unsupported parameter

In some cases opposite can be done as well, we may declare generic handlers which will accept any incoming messages:

import extensions;
 
class Example
{
    generic()
    {
        // __received is an built-in variable containing the incoming message name
        console.printLine(__received," was invoked")
    }
 
    generic(x)
    {
        console.printLine(__received,"(",x,") was invoked")
    }
 
    generic(x,y)
    {
        console.printLine(__received,"(",x,",",y,") was invoked")
    }
}
 
public program()
{
    var o := new Example();
 
    o.foo();
    o.bar(1);
    o.someMethod(1,2)
}

Output:

foo was invoked
bar(1) was invoked
someMethod(1,2) was invoked

We may declare a custom dispatcher which will redirect all unmapped incoming messages to another object effectively overriding it (some kind of dynamic mutation / code injection).

import extensions;

class Extender
{
    object theObject;
    
    // the injected property
    prop object Foo;
    
    constructor extend(o)
    {
        theObject := o
    }
    
    // redirect Object.Printable method to foo one
    get string Printable() => theObject;

    // custom dispatcher
    dispatch() => theObject;
}

public program()
{
    var o := 234;
  
    // adding a field
    o := Extender.extend(o);

    // setting a field value
    o.Foo := "bar";

    console.printLine(o,".foo=",o.Foo)
}

The output is:

234.foo=bar

The message may be dynamically dispatched.

class MyClass
{
    eval()
    {
       console.writeLine:"eval method"
    }                                                    

    state0()
    {
       console.writeLine:"state0 method"
    }
}  

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

   var subj := __subj state0;  // a message name constant

   o.eval();

   mixin subj(o).eval()        // dynamically dispatching the message
}

The output is:

eval method
state0 method

Though ELENA does not support multiple inheritance, using custom dispatcher we may simulate it:

singleton CameraFeature
{
    cameraMsg
        = "camera";
}
 
class MobilePhone
{
    mobileMsg
        = "phone";
}
 
class CameraPhone : MobilePhone
{
    dispatch() => CameraFeature;
}
 
public program()
{
   var cp := new CameraPhone();
 
   console.writeLine(cp.cameraMsg);
   console.writeLine(cp.mobileMsg)
}

The output is:

camera
phone

Now let's create a mixin object:

import system'dynamic;
import extensions;
 
class Member1
{
    string theField := "member1 content";

    field = theField;
}

class Member2
{
    eval()
    {
        // NOTE : target is built-in variable referring to the mixin
        console.printLine(
           "printing the content of the group object:",
           __target.field)
    }
}

public program()
{
    var g := Group.load(new Member1(), new Member2());
    g.eval();
}

Output is:

printing the content of the group object:member1 content

Using ClosureTape we may dynamically build the code:

import extensions;
import system'dynamic;

public program()
{
    var t := ClosureTape.new(new MessageClosure(mssgconst writeLine[1]));

    t(console,"Hello again")
}

The output is:

Hello again

The code may be simplified if we will build an expression tree:

import system'dynamic'expressions;

public program()
{
    var c := Expression.MessageCall(
                            mssgconst writeLine[1], 
                            Expression.Constant(console), 
                            Expression.Constant("Hello"));
    
    var t1 := c.compiled();
    
    t1()
}

The output is:

Hello

Or we may use TapeAssembly class to create a class in run-time:

import system'dynamic;
import extensions'dynamic;

 
public program()
{
    var a1 := ClosureTape.new(
        openSingletonClosure,
        openMethodClosure,
        new newMessageClosure("eval"),
        new newParamTokenClosure("self"),
        new newParamTokenClosure("n"),
        openCodeClosure,        
        openExpressionClosure,
        new newReferenceClosure("system'console"),
        new newMessageClosure("writeLine"),
        new newIdentifierClosure("n"),
        closeClosure,        
        closeClosure,
        closeClosure,
        closeClosure);
  
    var o := TapeAssembly.load(a1).eval();
    o.eval("Hello again")
}

Output is :

Hello  again

We may use TapeExpression to simplify the code. Let's create a simple class with a single method which counts down the parameter and prints the values:

import extensions'dynamic'expressions;

public program()
{
    // singleton Class
    var c := TapeExpression.Singleton(
    // method eval(object m)
                TapeExpression.Method(
                   "eval",
                   TapeExpression.Code(
   // var m := n.
                      TapeExpression.Declaring("m"),
                      TapeExpression.Assigning(
                        "m",
                        TapeExpression.Variable("n")
                      ),
   // while (m > 0)
                      TapeExpression.Loop(
                          TapeExpression.MessageCall(
                             TapeExpression.Variable("m"), 
                             "greater",
                             TapeExpression.Constant(0)),
                          TapeExpression.Code(
   // console writeLine(m)
                              TapeExpression.MessageCall(
                                 TapeExpression.Constant(console), 
                                 "writeLine",
                                 TapeExpression.Variable("m")),
   // m := m - 1
                              TapeExpression.Assigning(
                                "m",
                                TapeExpression.MessageCall(
                                    TapeExpression.Variable("m"),
                                    "subtract",
                                    TapeExpression.Constant(1)))))),
                      TapeExpression.Parameter("n")));

    var o := (c.compiled())();
    
    o.eval(5)
}

The output is:

5
4
3
2
1

Let's implement the following code dynamically:

{ test : n = if(n == 2)[^ 2]. ^ 0 }

To implement the condition we will use TapeExpression.If(condition, truePart) method :

import extensions;
import extensions'dynamic'expressions;

public program()
{
    var c := TapeExpression.Singleton(
                TapeExpression.Method(
                   "test",
                   TapeExpression.Code(
                      TapeExpression.If(
                        TapeExpression.MessageCall(
                            TapeExpression.Variable("n"),
                            "equal",
                            TapeExpression.Constant(2)
                        ),
                        TapeExpression.Code(
                            TapeExpression.Returning(
                                TapeExpression.Variable("n")
                            ))),
                      TapeExpression.Returning(
                        TapeExpression.Constant(0)
                      )),
                      TapeExpression.Parameter("n")));

    var t1 := c.compiled();

    var o := t1();

    console.printLine("test(2)=",o.test(2));
    console.printLine("test(3)=",o.test(3));
}

The output is :

test(2)=2
test(3)=0     

Now let's evaluate the code in run-time using ELENA Script Engine :

import extensions;
import extensions'scripting;

public program()
{
    escript.eval("system'console.writeLine(""Hello World"");");
    
    var o := escript.eval(
       "^ { eval(x) { ^extensions'math'mathControl.power(x, 2 ) }}");

    console.printLine(o.eval(2));        
    
    var o2 := escript.eval(
       "^ { eval(x,y) { ^x.add(y) }}");

    console.printLine(o2.eval(2,3));        
}

The output is :

ELENA VM 4.0.14 (C)2005-2019 by Alex Rakov
Initializing...
Done...
Hello World
4
5

Now we will invoke our code from IDE.

Let's write a simple class :

import extensions;

public singleton MyClassToTest
{
    foo()
    {
        console.printLine("foo fired!")
    }
}

public program()
{
}

Now let's compile the code : Project - Compile and open Interactive window : View - ELENA Interactive and type in the window (presuming test is the project root namespace):

>test'MyClassToTest.foo();

The output is :

ELENA command line VM terminal 4.0.6 (C)2011-2019 by Alexei Rakov
ELENA VM 4.0.10 (C)2005-2019 by Alex Rakov
Initializing...
Done...

>mytest'MyClassToTest.foo();
foo fired!

>