ELENA Programming Language

Alex Rakov edited this page Jun 14, 2018 · 56 revisions

1. 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 mix-ins.

1.1. Namespaces

Any ELENA program or library consists of modules ( files with .NL extension ) containing classes and symbols. Every member of the module is referred by its fully qualified name which consists of namespace and a proper name separated by an apostrophe. The namespace itself may contain sub elements separated by apostrophes.

All source files (files with .L extension) located in the same folder are compiled into the corresponding module. A project file ( a file with .PRJ extension ) defines the root namespace and the output type (stand-alone executable, VM executable or a library). The project may produce several modules if it contains the files located in sub folders (the new module namespace consists of the root one and the folder relative path is split by apostrophes).

1.2. Messaging

The main way to interact with objects in ELENA is sending a message. The message name is structured and consists of a operation and parameter signature and a parameter counter.

operation [parameter_attribute]* [parameter_counter]

e.g.

writeLine[2] - weak one
writeLine<LiteralValue,LiteralValue>[2] - strong one

The week message is used when the parameter signature cannot be resolved in compile-time, for example when the types of parameters are unknown.

If the object wants to handle the message it has to contain the method with the same name. If no method mapping was found the flow is considered to be broken and the control goes to the next alternative flow (exception handler) or the program is stopped.

The simple code to send a message looks like this:

console write:"Hello World".

Note: write is a weak message; a literal constant is a parameter. A dot is a statement terminator.

If the parameter is an expression itself it should be enclose in the round brackets:

console write:("Hello " add:"World").

In this case a colon may be dropped:

console write("Hello " add:"World").

Several messages can be send in one statement separated by semicolon:

console write:"2 + 2 ="; write(2 add:2).

We could use operators to have the shorter code:

console write(2 + 2).

Note: In most cases "+" is a synonym to add.

The message name can be composite:

console write:"Hello World" paddingLeft:10 with:$32

where the message name is write:paddingLeft:with[3].

The simple message can have several parameters as well:

console writeLine("a+b=",a + b).

1.3. Classes, Symbols, Nested classes and Closures

ELENA is an object-oriented language. To create a program we have to declare new classes and symbols.

A class encapsulates data (fields) with code (methods) to access it. In most cases it is not possible to get a direct access to the class content. Usually the field refers to another class and so on until we reach "primitive" ones which content are considered as raw data (e.g. numeric or literal values).

Classes form the inheritance tree. There is the common super class - system'Object. ELENA does not support multiple inheritance, though it is possible to inherit the code using a dispatch handler (mixins / group objects). When the parent is not provided the class inherits directly system'Object (the super class).

A class instance can be created with the help of the special methods - constructors. A constructor is used mostly to initialize the class fields. There are special types of classes which do not have constructors and can be used directly (singletons, nested classes, extensions, closures). A class itself is considered as a stateless object.

class BaseClass
{
  object theField1.
  object theField2.
  
  field1 = theField1.

  field2 = theField2.

}

class DerivedClass :: BaseClass
{
  constructor new:object1 and:object2
  [  
     theField1 := object1.
     theField2 := object2.
  ]

  add:object1 and:object2
     = MyClass new(theField1 + object1) and(theField2 + object1).
}

To create a class instance we have to send a message (usually new) to its class.

var anObject := DerivedClass new:1 and:1.

A symbol is a named expression and can be used to declare initialized objects, constants, reusable expressions and so on.

const N = 1. 

symbol TheClass = DerivedClass new:N and:N.

A static symbol is the named expression which state is preserved. There could be only one instance of static symbol.

static SingletonClass = DerivedClass new:0 and:0.

Nested classes can be declared in the code and used directly.

var ClassHelper =
{
   sumOf(object object1, object object2)
      = anObject1 add:object1 and:object2.
}.

...

var aSum := ClassHelper sumOf(anObject1,anObject2).

Closure is a special case of the nested class and consists of the code enclosed into square brackets (with optional parameter declaration)

str forEach:(:ch) [ console write:ch. ].

Note: a colon may be dropped:

str forEach(:ch) [ console write:ch. ].

The closure may be strong typed as well:

(:i)<int>[ ^ console print("Enter array[",i,"]:"); readLineTo(Integer new); value ]

When the closure contains only single return statement the code may be simplified:

(:i)<int>( console print("Enter array[",i,"]:"); readLineTo(Integer new); value )

1.4. Code blocks

ELENA code block consists of one or more declarations and statements. The block is enclosed in square brackets and may contain nested sub code blocks. The statement terminator is a dot.

0 to:MAX do(:i)<int>
[
    pi := pi + -1.0r power int:i / (2*i+1) * 4.
].

console writeLine:pi.
console writeLine("Time elapsed in msec:",aDiff milliseconds).

When a method should return a result (other than self) return statement is used. It should be the last statement in the block.

[
    ...

    ^ aRetVal / anArray length
]

If the code block contains only return statement the simplified syntax can be used:

Number = convertor toReal:theValue.    

It is possible to declare the block variable and assigns the value to it. The variable name must be unique within the code block scope.

var aRetVal := Integer new:0.

1.5. Conditional branching

Conditional branching is implemented with a help of special Boolean symbols (system’true and system’false). All conditional operations should return these symbols as a result.

There are three branching methods : if[1] , if:else[2], ifnot[1]

(m == 0) if:
[
   r append:(n + 1).
]
else:[
   r append:(m + n).
]).

This expression can be written using special operators

(m == 0) 
  ? [ r append(n + 1) ]
  ! [ r append:(m + n) ].

We could omit true or else part

(m == 0) 
   ! [ m / n ].

Boolean symbols supports basic logical operations (AND, OR, XOR and NOT), so several conditions can be checked

((aChar >= 48) and:(aChar < 58))
? [
    theToken += aChar
]
! [
   Exception new:"Invalid expression"; raise
]

if code template could be used:

if ((aChar >= 48) and:(aChar < 58))
   [
      theToken append:aChar
   ];
   [
     Exception new:"Invalid expression"; raise
   ].

Note that in this case both condition will be evaluated even if the first one is false. If we want to use short-circuit evaluation, lazy expression should be used:

if ((x >= 0)and:$(array[x] != 0))
[
    ...
]

Alternatively we may use primitive Boolean operators:

if ((x >= 0) && (array[x] != 0))
[
    ...
]

A switch statement can be implemented using => operator

aBulls =>
     -1 [ console writeLine:"Not a valid guess.". ^ true ];
      4 [ 
              console writeLine:"Congratulations! You have won!". 
              ^ false
          ];
        ! [
             theAttempt append:1.
             
             console printLine(
                  "Your Score is ", aBulls,
                  " bulls and ", aCows, " cows").
             
             ^ true
        ].

1.6. Hello world program

To write a simple console application, we have to declare the program main symbol - an object handling #closure[0] message. The simplest way is to declare a nested class:

public program =
{
    closure eval
    [
        system'console writeLine:"Hello World"
    ] 
}.

A nested class containing only one #closure method can be declared as a closure:

program =
[
    system'console writeLine:"Hello World"
].

Finally we may import system namespace:

import system.

program =
[
    console writeLine:"Hello World" .
].