Skip to content

ELENA in a nutshell

Aleksey Rakov edited this page Dec 16, 2020 · 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.

Here we will learn ELENA in details. Let's start!

Simple program

To create a simple program in ELENA we have to create a source file (with an extension .l) and write the following code:

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

We may compile it without creating a project file (where elc is a command-line compiler):

elc program1.l

The output is:

ELENA Command-line compiler 5.2.404 (C)2005-2020 by Alex Rakov

Project : program1, Platform: STA Win32 Console
Cleaning up...
Compiling...
Parsing unnamed
Compiling unnamed

Successfully compiled

Linking...
Successfully linked

It will create program1.exe file which we can execute:

program1

with the following result:

Here my first program in ELENA!

Beside the executable file a module program1.nl was created. Let's look what inside. We could use Bycode Viewer:

ecv program1.nl

The output will be:

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

>

Now let's look at the generated classes:

>?
class program

>

As you can see the program entry was compiled as a class. Let's look inside:

>program
@parent system'Object
@flag elSealed
@flag elStateless
@flag elRole
@method @function program.#invoke

>

Our function was compiled as a singleton with a single anonymous method #invoke. Let's dive more deep:

>program.1
@method program.#invoke
    open       1h
    pusha
    pushr      0
    pushr      0
    callr      system'console
    storesi    0
    movr       const : "Here my first program in ELENA!"
    storesi    1
    peeksi     0
    movm       mssgconst : "writeLine<system'String>[2]"
    callrm     system'$private'Console mssgconst : "writeLine<system'String>[2]"
    peekfi     1
    close
    quit
@end

>

What do we see? ELENA byte-codes are quite simple. Let's review some of them

opcode Description
pusha puts the object accumulator on the top of the function stack
pushr r puts a constant on the top of the function stack
callr r invokes a symbol
storesi i saves an object accumulator in the function stack at specified relative position
movr r assigns a constant to the object accumulator
peeksi i loads an object from the function stack at specified relative position to the object accumulator
movm m assigns a message constant to the data accumulator
callrm r m invokes a method m in r class

Once again let's look at our code. Now it is quite clear. We put console symbol and a string literal constant into the function stack and directly invokes system'$private'Console.writeLine<system'String>[2] method.

System and Program Entries

Is our program executed at the executable start? Let's look into the linker code:

// create the image
ident_t entryName = project->resolveForward(SYSTEM_ENTRY);
_entryPoint = emptystr(entryName) ? LOADER_NOTLOADED : linker.resolve(entryName, mskSymbolRef, true);
if(_entryPoint == LOADER_NOTLOADED)
    throw JITUnresolvedException(ReferenceInfo(SYSTEM_ENTRY));

where SYSTEM_ENTRY is defined as:

constexpr auto SYSTEM_ENTRY               = "$system_entry";                // the system entry

So the answer is no. There is a special entry which executes initialization code and invokes our program. The system entry is a symbol. Can I redefine it? Yes of course. Let's look at our code once again: $system_entry is resolved as a forward. What is a forward? In ELENA the forward reference is a reference which is resolved in linking-time. It means that this information should be provided when we compile the project.

Where $system_entry is defined? To find it out we have to look into how the project settings are configured in ELENA. When we compile it once again we will see that the following info:

ELENA Command-line compiler 5.2.408 (C)2005-2020 by Alex Rakov

Project : program1, Platform: STA Win32 Console

STA Win32 Console is a project template which settings we are copying from. This template can be based on another one and so on. The available templates are listed in elc.cfg configuration file:

<templates>
   <template key="console">templates\win32_console.cfg</template>
   <template key="mta_console">templates\win32_consolex.cfg</template>
   <template key="gui">templates\win32_gui.cfg</template>
   <template key="mta_gui">templates\win32_guix.cfg</template>
   <template key="vm_console">templates\vm_win32_console.cfg</template>
   <template key="lib">templates\lib.cfg</template>
</templates> 

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

<forwards>
   <forward key="$program">system'startUp</forward>
   <forward key="$system_entry">system'core_routines'sta_start</forward>
   <forward key="program">$elena'@rootnamespace'program</forward>
   <forward key="program'arguments">extensions'program_arguments</forward>
   <forward key="program'output">system'console</forward>
   <forward key="newLine">system'newLine</forward>
   <forward key="onStart">system'onConsoleStart</forward>
</forwards>

Here we are : $system_entry* is resolved as system'core_routines'sta_start reference. Let's look what inside:

>ecv system'core_routines
ELENA command line ByteCode Viewer 5.2.81 (C)2011-2020 by Alexei Rakov
C:\Alex\ELENA\lib50\system.core_routines.nl module loaded

>#sta_start    
>@symbol sta_start
    pushs      0
    callextr   extern : PrepareEM
    freei      1h
    open       1h
    reserve    5h
    pushs      -2
    callextr   $native'coreapi'initProgramHeader
    pushr      $forwards'$program
    pushr      $native'coreapi'seh_handler
    pushr      $native'coreapi'default_handler
    alloci     1h
    loadenv    
    savesi     0
    movf       -1
    callextr   extern : InitializeSTA
    freei      4h
    restore    7h
    close      
    quit       
@end

>

The relevant part of the code is invoking InitializeSTA function. This function initializes system environments and calls another forward symbol - $forwards'$program. Let's look once again into win32_console.cfg. $forwards'$program is defined as system'startUp. It is already manageable code. If it is redefined we will have our own system entry. Let's do it! We will provide a default exception handler to gracefully end the program in case of an error and wait until the user press any key

// system entry
public mySystemEntry = startUp();

// system boot code
startUp()
{
   try
   {
      // invoking a program entry
      program()
   }
   catch(Expression ex)
   {
      console.writeLine(ex.toPrintable())
   };

   // wait for any key
   console.readChar()       
}

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

Now we could compile the program:

elc program2.l -f$program=program2'mySystemEntry

When we execute the program it will wait until a user presses any key.

Defining a forward

Let's look into the system entry code:

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

>$private'startUp.1
@method $private'startUp.#invoke
    open       1h
    pusha      
    hook       Lab00
    movr       const : 'program
    movm       mssgconst : "#invoke"
    callrm     'program mssgconst : "#invoke"
    unhook     
    jump       Lab01
    Lab00:     nop
    popa       
    flag       
    and        200000h
    ifn        Lab02 0h
    load       
    peeksi     0
    callvi     0
    Lab02:     nop
    unhook     
    pushr      0
    pushr      0
    storesi    0
    movr       const : '$inline0
    storesi    1
    peeksi     0
    movm       mssgconst : "on[2]"
    callvi     0
    Lab01:     nop
    callr      system'console
    pusha      
    movm       mssgconst : "readChar[1]"
    callrm     system'$private'Console mssgconst : "readChar[1]"
    peekfi     1
    close      
    quit       
@end

The part, we are interested in, is the following:

    movr       const : 'program
    movm       mssgconst : "#invoke"
    callrm     'program mssgconst : "#invoke"

As you see the reference is resolved directly. This is not practical if we would like to reuse the system entry in different projects.

This is a typical use-case for a forward. First we have to rewrite the code slightly - by adding a forward attribute to the program reference :

// ...
startUp()
{
   try
   {
      // invoking a program entry
      forward program()
// ...

When we compile the code the result will be the same as before. Let's look once again in startUp:

@method $private'startUp.#invoke
    open       1h
    pusha      
    hook       Lab00
    callr      $forwards'program
    movm       mssgconst : "#invoke"
    callvi     0

As you see the reference was changed. It is no longer the direct one. But I do not define the forward. Why there was no error? The trick is in win32_console.cfg forward section. Look:

<forwards>
   <forward key="$program">system'startUp</forward>
   <forward key="$system_entry">system'core_routines'sta_start</forward>
   <forward key="program">$elena'@rootnamespace'program</forward>

First of all, program forward is already defined, it refers to $elena'@rootnamespace'program. Here comes the compiler magic - the namespace $elena'@rootnamespace is automatically replaced with the current project default namespace. In our case it is program2. Still we could easily redefine it. Let's implement alternative program:

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

And compile the project as follow:

elc program2.l -f$program=program2'mySystemEntry -fprogram=program2'alt_program

After the successful compilation the program will generate the following output:

Here my second program in ELENA!

Here we are. As you see it is quite easy to declare and resolve forward references in any ELENA program!

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.