Elan Language Reference

Instruction Set

This section defines the set of instructions that make up the Elan language and form the building blocks for any program.

There are three categories of instruction, which are distinguished where the instructions of that category are located within a program:

All instructions are added into a program using the new code ‘selectors’.

Comments

A comment is not an instruction: it is ignored by the compiler and does not change how the program works. Rather, a comment contains information about the program, intended to be read by a person seeking to understand or modify the code.

Every comment starts with the symbol # (known as ‘hash’) followed by some text or a blank line. Comments may be inserted at the same level as a global, member, or statement instruction, by entering # from the new code selector.

Every Elan program has a single comment at the top of the file, which is generated by the system and cannot be edited or deleted by the user. This comment is known as the ‘header’ and gives the version of Elan being run together with other optional information that depend upon your user profile.

Global Instructions

When you navigate to a new code that is at global level (not indented from the left-hand edge of the code pane) you will be shown the set of globals that you may insert there, namely all or only some of:

main procedure function test constant enum record class abstract interface #

where abstract is, in this context, short for abstract class. new code

The specific globals offered will depend upon your user profile.

Main method

A file must have a main method if it is intended to be run as a program. You may, however, develop and test code that does not have a main method, either as a coding exercise or for subsequent use within another program.

The main method defines the start point when a program is run.

The main method does not have to be at the top of the file, but this is a good convention to follow.

The main method may delegate work to one or more procedure or function.

There may not be more than one main method in a file – and the global selector (above) will not show the main option when one already exists in the file.

Example:
+main variable liname set to [3, 6, 1, 0, 99, 4, 67]expression call inPlaceRippleSortprocedureName(liarguments) print liexpression end main

Procedure

A procedure is a named piece of behaviour that can define parameters which are given inputs via arguments in a call statement. Unlike a function, a procedure does not return a value. Also unlike a function, a procedure can have ‘side effects’: indeed it should have side effects, otherwise there would be no point in calling it! For this reason the statements within a procedure can:

+procedure inPlaceRippleSortname>(out arr as List<of Int>parameter definitions) variable changesname set to trueexpression variable lastCompname set to arr.length() - 2expression +repeat set changesvariableName to falseexpression +for ivariableName from 0expression to lastCompexpression step 1expression +if arr[i] > arr[i + 1]condition then let tempname be arr[i]expression call arr.putAtprocedureName(i, arr[i + 1]arguments) call arr.putAtprocedureName(i + 1, temparguments) set changesvariableName to trueexpression end if end for set lastCompvariableName to lastComp - 1expression end repeat when not changescondition end procedure

Procedures are executed by means of a call statement, for example:

+main variable liname set to [3, 6, 1, 0, 99, 4, 67]expression call inPlaceRippleSortprocedureName(liarguments) print liexpression end main

Function

A function is a named piece of behaviour that can define parameters which are given inputs via arguments when reference to the function occurs in an expression. Unlike a procedure, a function returns a value. Also unlike a procedure, a function can have no ‘side effects’ and may not depend on any System methods.

Example of a function:

+function scorename(g as Gameparameter definitions) returns IntType return g.body.length() - 2expression end function

Parameters

Parameters for both procedures and functions are defined in exactly the same way: each parameter definition takes the form:
  <name> as <Type>
for example: age as Intparameter definitions

Recursion

Procedures and functions may be called or referenced recursively. For example, a simple factorial calculation: +function factorialname(n as Intparameter definitions) returns IntType return (if n > 1 then n*factorial(n - 1)
else
1)
expression
end function

Test

A test is a set of assertions, at the global level, about the output of functions. Example of a test, from the binary search demo program:

+test optional description let li1name be ["lemon", "lime", "orange"]expression assert binarySearch(li1, "lemon")computed value is trueexpected value pass assert binarySearch(li1, "lime")computed value is trueexpected value pass assert binarySearch(li1, "orange")computed value is trueexpected value pass assert binarySearch(li1, "pear")computed value is falseexpected value pass let li2name be ["lemon", "orange"]expression assert binarySearch(li2, "lemon")computed value is trueexpected value pass assert binarySearch(li2, "orange")computed value is trueexpected value pass assert binarySearch(li2, "pear")computed value is falseexpected value pass let li3name be ["lemon"]expression assert binarySearch(li3, "lemon")computed value is trueexpected value pass assert binarySearch(li3, "lime")computed value is falseexpected value pass let li4name be empty List<of String>expression assert binarySearch(li4, "pear")computed value is falseexpected value pass end test Notes:

Testing Float values

When testing Float values it is recommend that you always use the round function to round the computed result to a fixed number of decimal places. This avoids rounding errors and is easier to read:

+test round()optional description assert sqrt(2).round(3)computed value is 1.414expected value pass end test

Testing for runtime errors

If the expression you are testing causes a runtime error then the error will be displayed in the red fail message:

+test optional description let aname be [5, 1, 7]expression assert a[0]computed value is 5expected value pass assert a[2]computed value is 7expected value pass assert a[3]computed value is 0expected value actual: Out of range index: 3 size: 3 end test

If this occurs, mark the tests that you added since the last successful test run with ignore (see below), and then remove the ignores one by one until the cause is identified and can be fixed.

Marking a test with ‘ignore’

It is possible to mark a test with the ignore keyword, by selecting the test frame and then hitting Ctrl-i, as in this example:

+ignore test clockTickoptional description let g1name be newGame(new Random())expression let g2name be newApple(g1)expression let g3name be clockTick(g2, "s")expression assert g3.headcomputed value is newSquare(22, 16)expected value not run end test

When a test is marked with ignore, that test will not be executed when the tests are run, and its result will be shown as ‘not run’. The overall test status will also show in the ‘warning’ status (amber colour), even if all the tests that did run passed. This is to discourage you from leaving a test marked ignore for long.

The principal reason for marking a test ignore is when either the test code, or code in any function being called, does not terminate. This typically means that there is a loop (or a recursive call) with no exit condition, or where the exit condition is never met.

If you do create such code without realising it, then when the tests are executed the test runner will ‘time out’ after a few seconds (most tests will pass in milliseconds), and an error message will be shown in the System info pane. The test that caused the timeout will automatically then be marked ignore. Your priority should then be to identify the cause of the timeout and attempt to fix it before then restoring the test by selecting its frame and hitting Ctrl-i {which is a toggle for setting and unsetting an ignore status).

Constant

A constant defines a named value that cannot change and is always defined at global level (directly within a file) and is global in scope.

Examples: +constant phiname set to 1.618literal value or data structure +constant maxHitsname set to 10literal value or data structure +constant warningMsgname set to "Limit reached"literal value or data structure +constant fruitname set to {"apple", "orange", "banana"}literal value or data structure +constant blackname set to 0x000000literal value or data structure +constant redname set to 0xff0000literal value or data structure +constant coloursname set to {Suit.spades:black, Suit.hearts:red, Suit.diamonds:red, Suit.clubs:black}literal value or data structure +constant scrabbleValuesname set to {"A":1, "B":3, "C":3, "D":2, "E":1, "F":4, "G":2, "H":4, "I":1, "J":8, "K":5, "L":1, "M":3, "N":1, "O":1, "P":3, "Q":10, "R":1, "S":1, "T":1, "U":1, "V":4, "W":4, "X":8, "Y":4, "Z":10}literal value or data structure

In the colours example above, Suit is an Enum.

Enum

An enum – short for ‘enumeration’ – is the simplest form of ‘user-defined Type’. It specifies a set of values, each of which is defined as a name, such that a named value of Type enum necessarily always holds one of those values.

enums are read-only: once they have been defined it is not possible to add, remove, or update their values.

Examples: +enum SuitName spades, hearts, diamonds, clubsvalues +enum ActionName stand, drawvalues +enum OutcomeName undecided, win, lose, draw, winDoublevalues +enum StatusName pending, playing, standing, blackjack, bustvalues

Type name

The name given to an enum (see below), which must begin with an upper case letter, is used as the Type name when passing a value to or from a procedure or function.

Using an enum

The value is specified by the Type name for the specified enum, followed by a dot and the value name, for example: variable xname set to Status.pendingexpression

Record

A record is a user-defined data structure that is given a Type name (which must begin with an upper case letter). The record defines one or more properties, each of which has a name (starting with a lower case letter) and a Type. The Type of a property may be any simple value Type, or a ListImmutable, or another Type of record ( or even the same Type of record).

Note that the Type record has some similarity to a class in that:

However a record differs from a class in that:

Examples: +record SquareName property xname as IntType property yname as IntType end record +record GameName property headname as SquareType property bodyname as ListImmutable<of Square>Type property priorTailname as SquareType property applename as SquareType property isOnname as BooleanType property rndname as RandomType property graphicsname as BlockGraphicsType property keyname as StringType end record Having defined a record Type, such as Game above, you can create as many instances as you wish using the following syntax to specify the values: let g1name be new Game() with
head set to newSquare(22, 15),
key set to "d",
isOn set to true
expression
Note that you are not required to provide a value for each property because, where a property is not specified in the ‘with clause’ (as above), that property will be given the empty (default) value of the correct Type.

You can then read the values from the properties using ‘dot syntax’ for example:

print sq.sizeexpression

record Types are immutable: the properties on an instance may not be changed, directly. However, you can easily create another instance that is a copy of the original, with all the same property values except for any specific changes made in another with clause. The newly-minted copy (with changes) must be assigned to a new named value. For example:

let sq1name be new Square() with
x set to 3.5,
y set to 4.0,
size set to 1.0
expression
let sq2name be copy sq1 with
size set to 2.0,
colour set to red
expression
Or even to the same name if that name is a variable:

variable aname set to new Square() with
x set to 3.5,
y set to 4.0
expression
set avariableName to copy a with
x set to 3.7
expression

This last example shows how you enter the comma-separated with clauses. The earlier examples show how the Editor displays a set of with clauses.

If you want to use one or more existing property values in order to determine a new value, the property names must be prefixed with the name of the instance being copied, for example:

let sq2name be copy sq1 with
size set to sq1.size + 3
expression

Record deconstruction

A record may be ‘deconstructed’, meaning that its properties are read into separate variables using the same syntax as for deconstructing a Tuple. For example, assuming that Square is a record defined as in the example above, then this code:

let x, y, size, colourname be mySquareexpression

will read the properties into the four names defined.

When deconstructing, the names of the values must match the names of the properties of the record. However, the ordering of the names does not have to match the order in which the properties are defined in the record.

Class

See also: Inheritance

A class is a user-defined Type offering far richer capability than an enum.

Note that a record is in some ways similar to a class but simpler: it defines properties, but has no constructor and no methods. See Working with records.

Definition

Here is an example of class definition, taken from the Snake OOP demo program:

+class AppleName inherits ClassName(s) property locationname as SquareType +procedure newRandomPositionname(snake as Snakeparameter definitions) +repeat let ranXname be randomInt(0, 39)expression let ranYname be randomInt(0, 29)expression set property.locationvariableName to new Square(ranX, ranY)expression end repeat when not snake.bodyCovers(property.location)condition end procedure +function updateGraphicsname(gr as BlockGraphicsparameter definitions) returns BlockGraphicsType return property.location.updateGraphics(gr, red)expression end function end class

A class must have a name that, like any other Type, begins with an upper case letter.

A class may define:

Using a class

A class is instantiated using the keyword new followed by the class name and brackets, which should enclose the comma-separated arguments required to match the parameters (if any) defined on the constructor for that class. For example (also from the Snake OOP demo):

+constructor(parameter definitionsparameter definitions) let tailname be new Square(20, 15)expression set property.currentDirvariableName to Direction.rightexpression set property.bodyvariableName to [tail]expression set property.headvariableName to tail.getAdjacentSquare(property.currentDir)expression set property.priorTailvariableName to tailexpression end constructor

The created instance may then be used within expressions, like any other variable.

Inheritance

An ordinary class (also known as a ‘concrete class’) may optionally inherit from just one abstract class but may additionally inherit from any number of interfaces. The concrete class must define for itself a concrete implementation of every abstract member defined in the abstract class or any interfaces that it inherits from, directly or indirectly.

Notes:

Abstract Class

See also: Inheritance

An abstract class may not be instantiated (and hence may not define a constructor). It may define concrete members i.e.:

As with a concrete class, any of these members may be made private, after the corresponding frame has been added, by selecting that member frame and pressing Ctrl-p.

These concrete members are automatically inherited by any subclass, but they may not be overridden (re-defined) by the subclass. Therefore you should define concrete members only if they are intended to work identically on every subclass.

You may also define abstract methods on an abstract class, i.e. abstract property, abstract function, abstract procedure. Such methods define only the signature of the method, not the implementation (body), therefore they have no end statement. For example:

abstract function calculateDiscount() as Float

If you wish to have several subclasses of an abstract class that share a common implementation for a method, but require that some of the subclasses can define a different implementation, then you should:

Interface

See also: Inheritance

An interface is similar to an abstract class, with the difference that it may define only abstract members. The advantage of using an interface instead of an abstract class is that a concrete class can inherit from multiple interfaces. See Inheritance.

An interface may inherit only from other interfaces.

Important: An interface must not re-declare abstract interfaces that are defined in any interface it inherits from, directly or indirectly.

Member Instructions

When you navigate to a new code that is at member level (located directly within a record, class, abstract class, or interface) you will be shown the set of statements that may be inserted there, for example:

new code

The specific members offered will depend upon the context, and/or upon your user profile. The full set of entries is shown here, with links to explanations below:

constructor, property, procedure, function, abstract property, abstract procedure, abstract function, private property, private procedure, private function, #

Constructor

A concrete class may define a single constructor, which may:

If a class does define a constructor, and the constructor defines any parameters, then when the class is instantiated (using new) then values of the correct types must be provided, for example, if the class Snake defines this constructor:

+constructor(x as Int, y as Intparameter definitions) set property.xvariableName to xexpression set property.yvariableName to yexpression end constructor

then it may be instantiated like this:

let tailname be new Square(20, 15)expression

Property

Examples:

property height as Int
property board as Board
property head as Square
property body as [Square]

If the property is not initialised within the constructor then it will automatically be given the empty value for that Type. You may test whether a property contains this default value by writing e.g.:
if head is empty Square

constructor(board as Board)
set property.board to board
end constructor

procedure setHeight(height as Int)
set property.height to height
end procedure

Procedure Method

A ‘procedure method’ follows the same syntax and rules as a global procedure. The differences are:

Function Method

A function method follows the same syntax and rules as a global function>. The differences are:

Abstract Property

An abstract property may be defined only on an abstract class. Any concrete sub-class must then implement a concrete (regular) property to match.

Abstract Procedure Method

An abstract procedure method may be defined only on an abstract class. Any concrete sub-class must then implement a concrete (regular) property to match.

Abstract Function Method

An abstract function method may be defined only on an abstract class. Any concrete sub-class must then implement a concrete (regular) property to match.

Statement Instructions

When you navigate to a new code that is at statement level (located within a global or a member instruction) you will be shown the set of statements that may be inserted there, for example:

new code

The specific statements offered will depend upon the context, and/or upon your user profile. The full set of entries is shown here, with links to explanations below:

assert call each else for if let print repeat set throw try variable while #

Assert statement

Procedure Call

Each loop

Else clause

For loop

The loop counter variable does not have to have been defined in a variable statement.

The three defining values (from, to, and step) must all be integer, positive or negative.

They may be defined by literal integers, variables of Type Int, or expressions that evaluate to an integer.

However, if you require a negative step then the literal value, variable, or expression must start with a negative sign as this is needed at compile time to determine the nature of the exit condition. So if you have a variable s that holds a negative value to be used as the step, then you will need to write something like the following:

variable s set to -3
for i from 100 to 0 step -(-s)
  ..
end for

If statement

See also if expression

Example 1: +if head is applecondition then call setAppleToRandomPositionprocedureName(apple, bodyarguments) 'setAppleToRandomPosition' is not defined else if call body.removeAtprocedureName(0arguments) end if Example 2:
if item is value then
  set result to true
else if item.isBefore(value) then
  set result to binarySearch(list[..mid], item)
else
  set result to binarySearch(list[mid + 1..], item)
end if
Notes:

Let statement

Print statement

The simplest way to print is with the print statement. For example:
+main print "Hello"expression let aname be 3expression let bname be 4expression print a*bexpression print "{a} times {b} equals {a*b}"expression end main
Note:

Repeat loop

Set statement

Throw statement

You can deliberately generate, or ‘throw’, an exception when a specific circumstance is identified with a throw statement, for example:

throw exception "something has happened"

Try statement

Where another piece of code might throw an exception, for example when calling a System method that is dependent upon external conditions, it may be executed within a try statement, for example: +try call fooprocedureName() print "not caught"expression +catch exception in evariableName print eexpression end try The variable holding the exception (by default named e, but this may be changed by you) is of Type String. You may compare the exception message to one or more expected messages and, if the message does not match an expected exception, you may choose to throw the exception ‘up’, as in this example: +try call fooprocedureName() print "not caught"expression +catch exception in evariableName +if e isnt "an expected message"condition then throw exception emessage end if end try

Variable statement

While loop

Expressions

One of the most important constructs in programming is the ‘expression’. An expression is evaluated to return a value. An expression is made up of the following possible elements:

which this chapter describes.

Literal value

A literal value is where a value is written ‘literally’ in the code, such as 3.142 – in contrast to a value that is referred to by a name.

The following data Types may be written as literal values (follow the links to view the form of each literal value):

Int, Float, Boolean, String, List, ListImmutable, Dictionary, DictionaryImmutable, Tuple

Named value

A named value is a value that is associated with a name rather than being defined literally in code. There are various kinds of named value:

Constant, let statement, variable statement, Parameter passing, enum statement

Identifier

For all kinds of named values, the name must follow the rules for an ‘identifier’. It must start with a lower case letter, followed by any combination of lower case and upper case letters, numeric digits, and the _ (underscore) symbol. It may not contain spaces or other symbols. Once a named value has been defined, it can be referred to by the name.

Scoping and name qualification

With the exception of a constant (below), which is global in scope, named values are always ‘local’: their scope is confined to the method in which they are defined.

Elan allows local named values to be defined with the same name as a constant, function, or procedure defined at global level or defined in the standard library. In such cases, when the name is used within the same method, then it will refer to the local definition. If you have done this, but then need to access the constant, function, or procedure with the same name, then you can simply prefix the use of the name with a ‘qualifier’ of either global. or library. as appropriate.

Indexed Value

If a variable is of an indexable Type, then an index or index range may be applied to the variable within an expression. For example:
    variable a set to "Hello World!"
    print a[4]o
    print a[4..]o World!
    print a[..7]Hello W   (since the upper bound of a range is exclusive)
    print a[0..4]Hell         (for the same reason)

See also: Using an List, Using a Dictionary

Important: unlike in many languages, indexes in Elan (whether, single, multiple, or a range) are only ever used for reading values. Writing a value to a specific index location is done through a method such as in these examples:

    putAt            on a   List
    withPut        on a    ListImmutable
    putAtKey      on a    Dictionary
    withPutKey  on a    DictionaryImmutable

Operators

Arithmetic operators

Arithmetic operators can be applied to Float or Int arguments, but the result is always a Float:

    2^38
    2/30.666...
    2*36
    2 + 35
    2 - 3-1
    11 mod 32 (integer remainder)
    11 div 33 (integer division)

Arithmetic operators follow the conventional rules for precedence i.e. ‘BIDMAS’ (or ‘BODMAS’).

When combining div or mod with any other operators within an expression, insert brackets to avoid ambiguity e.g.:
    (5 + 6) mod 3
The minus sign may also be used as a unary operator, and this takes precedence over binary operators so:
    2*-3-6

Note that the Elan editor automatically puts spaces around the + and binary operators, but not around ^, / or *. This is just to visually reinforce the precedence.

Logical operators

Logical operators are applied to Boolean arguments and return a Boolean result.

and and or are binary operators
not is a unary operator.

The operator precedence is notandor, so this example, which implements an ‘exclusive or’, need not use brackets and can rely on the operator precedence:

+function xorname(a as Boolean, b as Booleanparameter definitions) returns BooleanType return a and not b or b and not aexpression end function

Equality testing

Equality testing uses the is and isnt keywords with two arguments. The arguments may be of any Type.

Numeric comparison

The numeric comparison operators are:

    >         for     greater than
    <         for     less than
    >=        for     greater than or equal to
    <=        for     less than or equal to

Each is applied to two arguments of Type Float, but any variable or expression that evaluates to an Int may always be used where a Float is expected.

Notes:

Combining operators

You can combine operators of different kinds, e.g. combining numeric comparison with logical operators in a single expression. However the rules of precedence between operators of different kinds are complex. It is strongly recommend that you always use brackets to disambiguate such expressions, for example:

(a > b) and (b < c)expression
(a + b) > (c - d)expression

Function call

An expression may simply be a function call, or it may include one or more function calls within it. Examples: print sinDeg(30)expression variable xname set to sinDeg(30)^2 + cosDeg(30)^2expression variable namename set to inputString("Your name")expression print name.upperCase()expression Notes:

Lambda

A lambda is lightweight means to define a function ‘in line’. You typically define a lambda:

Example: +function liveNeighboursname(cells as List<of Boolean>, c as Intparameter definitions) returns IntType let neighboursname be neighbourCells(c)expression let livename be neighbours.filter(lambda i as Int => cells[i])expression return live.length()expression end function Notes:

The following example uses both these techniques within a function:

+function safeSquareRootname(x as Floatparameter definitions) returns FloatType let rootname be lambda => sqrt(x)expressionname as Type, ... return if x < 0 then 0
else
root()
expression
end function

If expression

The ‘if expression’ is in certain respects similar to an If statement, but with the following differences:

Here are three examples:

return if c < 1160 then c + 40
else
c - 1160
expression

return if isGreen(attempt, target, n) then setChar(attempt, n, "*")
else
attempt
expression

return if attempt[n] is "*" then attempt
else
if isYellow(attempt, target, n) then setChar(attempt, n, "+")
else
setChar(attempt, n, "_")
expression
Elan Language Reference go to the top