-
-
Notifications
You must be signed in to change notification settings - Fork 26
ELENA in a nutshell
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).
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.
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!
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
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.
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.