Skip to content

ELENA in a nutshell

Aleksey Rakov edited this page Sep 5, 2024 · 32 revisions

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.

In this series of posts we will learn ELENA in details. Let's start!

Hello world example

We will begin with "Hello, World!" program. To do it let's create a source file (e.g. "sample1.l") and write the following code:

public program()
{
   console.writeLine("Here my first program in ELENA!")
} 

To compile the program we can use ELENA command-line compiler elena-cli or elena64-cli (for 64 bit version). For our simplest case we need only to provide a path to the source file, like this:

elena-cli.exe sample1.l

If everything is setup correctly, the compiler will generate the following output:

ELENA Command-line compiler 6.0.595 (C)2005-2024 by Aleksey Rakov
Project: sample1, Platform: Win_x86, Target type: STA Console
Cleaning up
Parsing sample1.l
Compiling sample1.
saving sample1

Successfully compiled
Linking..
Successfully linked

As you can see the output contains the compiler version (it is important to provide this version by reporting any bug), the project name (in our case it coincides with the source file name), the platform (Win_x86 - is windows 32 bit version) and the type of the application (STA Console stands for "single thread console application"). If the code contains no error and the module was created (sample1.nl in our case), the message "Successfully compiled" is printed. "Successfully linked" indicates that our project has generated an executable file with a name "sample1.exe".

And now it can be executed:

>sample1.exe
Here my first program in ELENA!

The source code is quite simple and easy to understand. For the console application the main program must be declared inside a public function with a name program. In our case the program prints a string to the screen. console is a special object implementing basic operations with a console (we call it a symbol). The method writeLine does exactly what we expect : prints the string and moves the cursor to the new line.

As it was mentioned above the compiler generates a special file (with .nl extension) which in its turn is used by the linker to generate the program. This module contains the list of compiled classes (in our case the function is a special case of a class). So let's look inside it. For this a special tool - ecv-cli (Byte-code viewer) - can be used.

Type the following command:

ecv-cli sample1.nl

and the output will be following:

ELENA command line ByteCode Viewer 6.0.81 (C)2011-2023 by Aleksey Rakov
module sample1 loaded
namespace : sample1
name      : 
version   : 
author    : 

list of commands
?                       - list all classes
<class>                 - view class members
<class>.<message>       - view a method byte codes
<class>.<index>         - view a method specified by an index byte codes
#<symbol>               - view symbol byte codes
-b                      - toggle bytecode mode
-h                      - toggle method hints mode
-p                      - toggle pagination mode
-q                      - quit
-t                      - toggle ignore-breakpoint mode

>

To see the main program let's type:

>program

@parent  system'Object
@flag  elClosed
@flag  elFinal
@flag  elRole
@flag  elSealed
@flag  elStateless
#1: @function program.function:#invoke

As it was said above, we see that the function is in fact a class. Flag elStateless indicates that it is a singleton. And it contains a special method - function[0]

We can print its content as well:

>program.1

>@function program.function:#invoke
           xflush       sp:0
           open           :4, :0
           store        fp:1
           call     symbol:system'console
           store        fp:2
           xstore       sp:1, strconst:Here my first program in ELENA!
           peek         fp:2
           store        sp:0
           mov        mssg:writeLine[2]
           call       mssg:writeLine[2], class:system'Console
           peek         fp:1
Lab00:     nop
           close          :0
           quit           
@end

>

The program is encoded with byte-code - intermediate code which is translated by JIT compiler either during the program linkage or on the fly by the virtual machine. Here is a list of some of them:

opcode Description
open opens a new procedure frame
store saves an object accumulator into the stack
call calls a function
mov assigns a constant to the data accumulator
peek loads an object from the stack
close closes the current procedure frame and restore the previous
quit exits the procedure

At the moment we don't need to understand it exactly, it is enough to take a first look at it. The code is simple. The console symbol and a literal constant are stored into the stack. After that the message writeLine[2] is send to the console (inside the square brackets is the number of arguments including a target class).

System and Program Entries

To proper set up the environment a program prologue must be called. Often after the program is done, the resources have to be freed, that's why we need a program epilogue. All this done in ELENA with introducing a system entry. A system entry is a special symbol which executes all preparation work, invokes the program entry and unwind the system aftermath.

The program entry itself is a symbol. For simplicity we can say that a symbol is a named expression. For the console application the program entry invokes the wrapper code which put our main program entry inside try-catch block to allow graceful exit even if the program fails.

Where is the program entry symbol is defined? When our program was compiled the compiler generates the following info:

ELENA Command-line compiler 6.0.552 (C)2005-2023 by Aleksey Rakov
Project: program1, Platform: Win_x86, Target type: STA Console

STA Console is a project template used to compile the console application. The template can be based on another one and so on. The available templates are listed in elc60.cfg configuration file:

 <templates>
    <template key="console">templates\win_console60.cfg</template>
    <template key="gui">templates\win_gui60.cfg</template>
    <template key="lib60">templates\lib60.cfg</template>
    <template key="vm_console">templates\vm_win_console60.cfg</template>
    <template key="mt_console">templates\mt_win_console60.cfg</template>
 </templates>

So let's look into the forward section of win_console60.cfg :

 <forwards>
    <forward key="$system_entry">system'core_routines'sta_start</forward>
    <forward key="$symbol_entry">system'$private'entrySymbol</forward>
    <forward key="program">$rootnamespace'program</forward>
 </forwards>

$symbol_entry is used by the compiler to resolve the program entry. We can find the code inside app.l source:

entry()
{
   try
   {
      forward program();
   }
   catch::
   {
      function(AbortException e)
      {
      }
    
      function(Exception err)
      {
         console.writeLine(err);

         extern ExitLA(-1);
      }
   }
}

private entrySymbol
   = entry();

As you see the entry symbol invokes the function entry. Inside the function we can see try-catch block around the proper program entry call:

forward program();

With a help of the compiler magic, forward program is resolved automatically. All we need is to declare a function named program inside the program main namespace.

Is it possible to override the program entry? Yes. We can easily define our own entry wrapper. Let's do it. For example we can define the entry which will print the result of the main program.

First our main program has to be modified to return a result.

public program()
   = "the program result";

Then we define the public symbol which executes the program and prints the result

public mySystemEntry =
   console.printLine("My program returns ", forward program());

And we have to tell the compiler that we have a new program entry:

elena-cli program2.l -f$symbol_entry=program2'mySystemEntry

And the output is

My program returns the program result

Variable declaration

A variable is a named place in the program memory (for example in the function stack). We can assign a value to it, read this value or change it. In the simplest case to declare variable we have to use var attribute:

var myFirstName;

We can assign a value to it:

myFirstName := console.readLine();

Note that it is a first point where ELENA syntax diverges from C-style one and follows in footsteps of Pascal and Smalltalk. The difference between := (assignment) and = (equivalence) does play a role in the language.

We can make both a declaration and an assignment in one line:

var mySecondName := console.readLine();

The variables are used to store some values for further use in the code. So let's do it:

import extensions;

public program()
{
   console.write("Enter your first name:");
   var myFirstName;
   myFirstName := console.readLine();

   console.write("Enter your last name:");
   var myLastName := console.readLine();

   console.printLine("Hello, ", myFirstName, " ", myLastName);
}    

Classes and functions

ELENA is an object-oriented programming language. So the code must be declared inside one or another class. A class is a combination of a data (class variables or fields) and a code (class methods). For the language with late binding another concept is important - a message. The message is an request sent to an object (an instance of the class) to perform an operation (a class method). It is done with a help of a special method - the message dispatcher. The default dispatcher resolves the request with a help of a method table - a list of mapping between the message and the code.

So our operation

console.write("Enter your first name:");

can be interpreted as an operation of sending a message write[2] (where a number inside the square brackets indicate a number of arguments plus the message target) to the object console.

Note: it is important to take this into account when considering how the method call is implemented in ELENA, in the most generic case the actual code which is invoked are defined inside the target dispatcher. The message is passed as a special constant and it is possible to perform some manipulation with it

So how we can declare a class. It is quite straight-forward:

class MyFirstClass
{ 
   field myFirstField;
   
   method doMyFirstOperation()
   {
      console.writeLine("My first method is invoked"); 
   }
}

An attribute class indicates that we declare a class. field marks the class variable - a field named myFirstField. And method myFirstMethod() contains the method code which prints the greeting from our first class.

To use the object we have to create an instance of it by calling a constructor.

public program()
{
   var myFirstObject := new MyFirstClass();

   myFirstObject.doMyFirstOperation();
}

The output will be:

My first method is invoked

A function in ELENA is a special case of an object which contains only one method and cannot have fields (it is called stateless or a singleton).

program is an example of a function containing the main body of the program. A function as well as method can have arguments. The arguments are placed inside brackets and are comma separated:

function invokeMyClass(myObject)
{
   myObject.doMyFirstOperation();
}

Note: an optional attribute function can be used

To call a function all we need is to write a function name placing arguments inside brackets:

public program()
{
   invokeMyClass(new MyFirstClass());  
}

Object interactions

In dynamic programming languages the process of invoking a method is called a message sending. We can define a message as some numeric number which is mapped to an address of some code which should be called to handle it. The mapping is called a method table. In normal case the method table is defined for a class. Let's look how all of these are organized in ELENA.

A dynamically allocated object consists of a body and a header. The header contains several special fields, one of them is a reference to an object class. In ELENA the class is an object itself and has a body as well. The class body is in fact a method table. So to send a message to the object we have to get its class, look into its method table to find matched method address and call it. To speed up this process the method table can be sorted so a binary search can be used. It makes sense to provide a special method (usually called a dispatcher) which will do it to simplify the operation. The compiler will guarantee that the dispatcher is always the first entry in the method table. So now our operation looks like this: take the object class and call the first method in its method table passing a message as an extra parameter.

So let's write an example. We will declare a class and defines the method foo which will accept a parameter. The method will print the parameter. The main program will declare a weak variable which is assigned to an instance of class A. We will send a message foo with a string literal as a parameter to the declared variable:

A
{
   foo(param)
   {
       console.writeLine(param)
   }
}

public program()
{
   var a := new A();
   a.foo("bar")
}

We can compile the code:

elc program3.l 

and look into the generated code:

>ecv program3.nl

ELENA command line ByteCode Viewer 5.2.82 (C)2011-2020 by Alexei Rakov
program3.nl module loaded

>program.1
@method program.#invoke
    open       1h
    pusha      
    pushr      0
    movr       class : '$private'A
    pusha      
    movm       mssgconst : "#constructor[1]"
    callrm     '$private'A#class mssgconst : "#constructor[1]"
    storefi    2
    pushr      const : "bar"
    pushfi     2
    peeksi     0
    movm       mssgconst : "foo[2]"
    callvi     0
    peekfi     1
    close      
    quit       
@end

>

We will look into the code corresponding to the second source line:

    pushr      const : "bar"
    pushfi     2
    peeksi     0
    movm       mssgconst : "foo[2]"
    callvi     0

The first two line is straight-forward: we put into the call stack the message parameter and the message target. Then we load a message target to the VM accumulator. The last two lines are more interesting: we put into an indexer register a message id and calls the first method in the object method table.

Could we look into the dispatcher code? Sure. The default dispatcher is declared in the super class - system'Object.

>ecv system

ELENA command line ByteCode Viewer 5.2.82 (C)2011-2020 by Alexei Rakov
system module loaded

>Object
@classinfo a common ancestor
@method Object.equal[2] of 'BoolValue;;o|Returns true if the specified object is equal to the current object; otherwise, false. By default compares the object references.
@method Object.notequal[2] of 'BoolValue;;o|Returns true if the specified object is not equal to the current object; otherwise, false. By default it inverts the result of equal[2].
@method Object.toPrintable[1] of 'String;;Returns the string representation. By default it returns the class name.
@method Object.#dispatch[1];;Implements a message dispatching in VMT

We are interested in the last one, so

>Object.4
@method Object.#dispatch[1]
    bsredirect 
    open       4h
    reserve    2h
    savesi     3
    pushs      -3
    pusha      
    movr       class : system'Message
    pushr      class : system'MethodNotFoundException
    storesi    5
    peeksi     0
    pushr      mssgconst : "new[3]"
    throw      
@end

The first opcode is the most important one - it looks into the accumulator method table, searches for a message in the index register and jumps to the corresponding method. So when the invoked method will return the control, the next opcode after callvi will be executed by-passing the rest of the dispatcher code. But if no corresponding method will be found the rest of the dispatcher will be executed - throwing MethodNotFoundException exception.

So now let's executed our program:

>program3
bar

Multimethods

In dynamic language you may always ignore types and pass any object as you wish.

import extensions;

A
{
    bar()
    {
        console.printLine("bar")
    }
}
    
singleton C
{
    proceed(o)
    {     
        console.print(o,":");
        o.bar()
    }
}

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

The result will be:

'$private'A:bar

But in many cases we do need to implement different routines for the different objects. The simplest way would be of course to check the object type:

import extensions;

A
{
    bar()
    {
        console.printLine("bar")
    }
}
    
B
{
    foo()
    {
        console.printLine("foo")
    }
}

singleton C
{
    proceed(o)
    {
        console.print(o,":");
        if (o.instanceOf(A))
        {
            o.bar()
        }
        else if (o.instanceOf(B))
        {
            o.foo()
        }        
    }
}

public program() 
{
    var a := new A();
    var b := new B();
    C.proceed(a);
    C.proceed(b);
}

And the result is:

'$private'A:bar
'$private'B:foo

The problem is that it will not work for mixings for examples. And in general it is far from being an elegant solution.

Alternatively we could resolve request by clarifying the required operation. For example :

program                        C                              A
|
+--------- proceed(A) -------->+
                               |
                               +------- reactToC(C) --------->+
                                                              |
                               +<-------- proceedA(A) --------+
                               |
                               ...

This approach can be used with mixins as well:

import extensions;
import extensions'scripting;

A
{
    bar()
    {
        console.printLine("bar")
    }
    
    reactToC(c)
    {
        c.proceedA(self)
    }
}

B
{
    foo()
    {
        console.printLine("foo")
    }
    
    reactToC(c)
    {
        c.proceedB(self)
    }
}

singleton C
{
    proceedA(a)
    {
        a.bar()
    }
    
    proceedB(b)
    {
        b.foo()
    }
    
    proceed(o)
    {
        console.print(o,":");
        o.reactToC(self)
    }
}

public program() 
{
    var a := new A();
    var b := new B();
    
    var cloneOfA := lscript.interpret(
    "{
        bar() { system'console.writeLine(""bar from clone""); }      
        
        reactToC(c) { c.proceedA(self); }  
    }");
    
    C.proceed(a);
    C.proceed(b);
    C.proceed(cloneOfA);
}

The result will be:

'$private'A:bar
'$private'B:foo
'DynamicClass:bar from clone

Unfortunately this approach cannot be easily scaled up even for two arguments.

Multimethods and mixins

The best way to dispatch the operation is to provide the expected argument types. The compiler will try to resolve such operations in compile-time if there is enough information. Then the argument type is not known the special multi-method will be created. Let's try it:

import extensions;

class A
{
    bar()
    {
        console.writeLine("bar")
    }    
}

class B
{
    foo()
    {
        console.writeLine("foo")
    }    
}

singleton C
{
    // declaring multi-method proceed
    proceed(A a)
    {
        console.print(a,":");
        a.bar()
    }
    
    proceed(B b)
    {
        console.print(b,":");
        b.foo()
    }
    
    // default handler
    proceed(o)
    {
        console.printLine(o,": unknown");
    }
    
}

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

    C.proceed(a);
    C.proceed(b);
}

The result will be:

'$private'A:bar
'$private'B:foo

Let's look at singleton C:

> ecv program7a.nl
ELENA command line ByteCode Viewer 5.7.92 (C)2011-2021 by Alexei Rakov
program7a.nl module loaded

>$private'C
@parent system'Object
@flag elSealed
@flag elStateless
@flag elRole
@method $private'C.proceed<'$private'A>[2];;a|
@method $private'C.proceed<'$private'B>[2];;b|
@method @multidispatcher $private'C.proceed[2];;o|

As we see a special multi-dispatcher was generated. Let's look at it:

>$private'C.3

@method $private'C.proceed[2]
    xmtredirect'proceed$inline0 mssgconst : "proceed[2]"
    open       0h
    pusha      
    // ... 
    peekfi     1
    close      
    quitn      2h
@end

The important part is xmtredirect opcode. It will try to dispatch the incoming message based on the argument types. If no match was found, the default code will be executed.

Passing the argument type directly is not the best strategy, so it would be better to introduce interfaces, so we could implement for example decorator pattern.

interface IA
{
    abstract bar();    
}

interface IB
{
    abstract foo();    
}

class A : IA    
{
    bar()
    {
        console.writeLine("bar")
    }    
}

class B : IB
{
    foo()
    {
        console.writeLine("foo")
    }    
}

singleton C
{
    proceed(IA a)
    {
        console.print(a,":");
        a.bar()
    }
    
    proceed(IB b)
    {
        console.print(b,":");
        b.foo()
    }

// ..

This approach works quite well. But what if we would like to introduce a mixin. For example a generic decorator:

Decorator
{
    target;
    
    constructor(target)
    {
        this target := target
    }
    
    generic()
    {
        console.print("decorating ");
        
        __received(target)
    }
}

public program()
{
    var a := new A();
    var b := new B();
    var decA := new Decorator(a);

    C.proceed(a);
    C.proceed(b);
    C.proceed(decA);
}

Unfortunately it will not work:

'$private'A:bar
'$private'B:foo
'$private'Decorator: unknown

To solve this problem we have to add a conversion routine to our Decorator class, which will generate an interface bridge:

Decorator
{
    target;
    
    // ...
 
     IA cast()
        = new IA
        {
            embeddable dispatch() => self;
        };
}

And of course typecast our mixin:

var decA := cast IA(new Decorator(a));

Now, our code works well:

'$private'A:bar
'$private'B:foo
'$inline0:decorating bar

When we will look at Decorator conversion routine:

@method $private'Decorator.#cast<'$private'IA>[1]
    open       0h
    pusha      
    new        class : '$inline0 1
    xsetfi     1 0
    close      
    quitn      1h
@end

we will see that a proxy class $inline0 was generated by compiler to provide interface dispatcher:

@method $inline0.bar[1]
    geti       0h
    movm       mssgconst : "bar[1]"
    jumpvi     0
@end

the generated code is quite simple, it is just redirect the incoming message to the proxy field.

But what to do with the second interface. Of course we could provide the second conversion routine. But there is a better solution - generic conversion:

Decorator
{
    // ...

    generic cast()
    {
        var type := __received.__getFirstSignatureMember();
        
        var proxyType := type.__inheritProxy();
        var proxy := proxyType.__newProxy(self);
        
        ^ __received(proxy);
    }
}

The method will be invoked every time we have to typecast the decorator. We can retrive the expected interface from the received message:

var type := __received.__getFirstSignatureMember();

After this we can dynamically inherit this interface and create a proxy class:

var proxyType := type.__inheritProxy();
var proxy := proxyType.__newProxy(self);

So our final code will look like this:

public program()
{
    var a := new A();
    var b := new B();
    var decA := cast IA(new Decorator(a));
    var decB := cast IB(new Decorator(b));

    C.proceed(a);
    C.proceed(b);
    C.proceed(decA);
    C.proceed(decB);
}

And the output is:

'$private'A:bar
'$private'B:foo
'$1:decorating bar
'$2:decorating foo

Using this approach we can use mixins to implement various types of mockups and decorators.