Gene logo      Menu
Gene name
Introduction
Documentation
Download
 
Development
Bugs
News

Language specification

  1. Syntax
  2. Keywords
  3. CodeUnits and Projects
  4. Include
  5. Private, public and global
  6. Comments
  7. pragma
  8. Simple types
    1. Base types
    2. enumerators
    3. structures
      1. Variables
      2. Properties
      3. expression declerations
      4. Overloaded operators
      5. Constructors
      6. Destructors
      7. Send message
    4. Unions
  9. Complex types
    1. Spaces
      1. Variables
      2. Properties
      3. expression declerations
      4. Overloaded operators
      5. Constructors
      6. Destructors
      7. Send message
      8. receive message from object
      9. GetNext
    2. Stores
  10. Type declerations
  11. Renames
  12. global variables and constants
  13. Processes and Libraries
  14. Automations
    1. Using automations
    2. Creating automations
  15. The messaging system
  16. Statements
    1. Variable decleration
    2. Assignments
    3. Shift statements
    4. Copy and move statements
    5. Expression call
    6. Interface statements
    7. return statements
    8. Options
    9. Condition blocks
    10. Loops
    11. Break and Continue
    12. Inherited
    13. Constructor call
    14. Destructor call
    15. raising exceptions
    16. Exception handling
      1. Except statement
      2. finally statement
  17. Designators
  18. Casts
  19. Space filters
  20. Operators
    1. Aritmetic
    2. Assignment
    3. Bitwise
    4. Logical
    5. At
    6. Reference - dereference
    7. Relational
    8. Type operators
    9. Precedence rules
  21. assembler blocks
  22. Escape sequences

Keywords

abstract
Entry
Inline
override
rename
true
asm
Enum
inherited
out
return
try
auto
Exp
is pascal
self
var   
break
Extern
Last Prev
sizeof
virtua
chapter
Except
Lib Process
shl
write 
cdecl
false
loop Prop
shr
union
const
fastcall
message
Private
Static   hook
Constructor
finally
Next  Pos
stdcall Os
continue
First
Nil
Public
Store Platform
default
In
nrrecords
raise
Struct
Destructor
Index
of
read
Space
Entity
Include
operator
register
Type

Code Units and Projects

Gene uses code units as the input for the compiler.  Code units are text files with the extention 'gen', containing source code.  A project is the main code unit that is being compiled.  For all the processes and libs on the project unit an output will be generated.

Code units are linked together through the use of the include statement

Include

Include = "include" UnitName {"," UnitName} ";"
UnitName = string

The include statement is used to list the code units that you want to use in another unit.  This allows you to split up a project in several smaller files containing related code.  It also allows you to reuse common code by putting it in a seperate unit and storing it in the common directory.

The Gene compiler looks for code units in the project directory (the directory containing the project file you are currently trying to compile) and it uses a predefined directory to look for common code units.  This is currently the 'units' subdir of where the compiler is located.

The include statement can appear everywhere in the source code at the base level (not nested) and can be written as many times as you like.  Furthermore it is perfectly legal to write circular references, that is, unit 'a' may include unit 'b' which includes unit 'a'. This allows you to split up the code units more efficiently.  Code units can also be included more than 1 time in the same code unit.  This allows you to create a sort of ping pong effect.

 Examples:

include StdIo, files;
include baseStore;

Loading sequence explenation
Code unit a:
include b;
//do1
include b;
//do2

Code unit b:
//do3
include a;
//do4

will result in the following sequence:
do3
do1
do2
do4

Private, public and global

The private, public and global keywords are used to switch between unit local, unit public and global code.  By default, all code in a code unit is known to all the other units that include it (public). 

When the private keyword is incountered, the declerations that follow can only be accessed from within the same unit but not outside of it. 

When the global keyword, is encountered, all the declerations from thereon are known through the intire project, also in the units that don't explicitly include the unit that contains the decleration.

These keywords can not appear in nested structures, only at the base level (at the same level as process, lib and include).


example:
In this example,  space y can only be used within the code unit it was defined in while space z is known in the intire project.

public
space x =
var a: int
;
private
space y =
var a: bool
;
global
space z =
var a: pointer
;

Comments

 Gene supports 2 types of comments:
Comments can appear anywhere in the code.

examples:

//line comment
include a; //another line comment
/* a block comment that starts here
and ends here */

Pragma

pragma 
 [ "interpretedict" ["on" | "off" | "excl"]
 | "pack" [ ["push" | ] [1|4|8|16]  | "pop" ]
 | ["freelib" | "loadlib" | "procaddress" | "alloc" | "free"] designator
 | "ProcessInstance" designator
| "UniCode" ["on" | "off"]
 ] ";'

The pragma keyword allows you to configure the compiler settings from wihin the code.  The compiler must recognise the instruction, otherwise an error message is generated.
The following pragma statements are recognised:

Simple types

Simple types are types that do not have type information stored in them.. This includes the base types (such as bool, int,..) and the structured types such as unions and structures.

For more information, see:

  1. Base types
  2. enumerators
  3. structures
  4. Unions

Base types

 Gene contains the following base types:
Name
signed
Nr of bits floating point
char
true
8
no
byte
false
8
no
short true
16
no
word false
16
no
int true
32
no
dword false
32
no
long true
64
no
qword false
64
no
float true
32
yes
double true
64
yes
extended true
80
yes
bool false
32
no
pointer false
32
no
IdType false
32
no

the 'IdType' is able to store run time type information of other objects.  

For instance:

a: int
b: IdType
b = a //b contains the type of a
b = bool //b contains bool type
[<-b == int-> //if b cotnains type int, do...
a = 1
]
b = true //illegal code

These base types are valid for the compiler.  In the case of the interpreter, all types are reduced to the following internal types: dword, int, long, qword, extended which have the same properties as their external counterparts.

enumerators

"enum" Identifier "=" [Identifier ["=" integer] {"," Identifier ["=" integer]}] {ExpDecleration| MessageSend}  ";"

The enum keyword allows you to define a set of constants of type int.  Internally, all enumerators are presented as integers of 32 bits.  Any int value can be assigned to the enumerator, no checking is performed.   Enumerators can also be sent to message receivers.

example:

enum ObjectType = Point = 1, square, triangle
exp reset = self^ = square ;
;

structures

"struct" Identifier ["(" [ Identifier {, Identifier} ] ")"] "="
{ Variables
|Properties
|Expression declerations
|overloaded operators
|Constructors
|Destructors
|MessageSendDefs
}
";"

A structure is used to group several variables into 1 record that can be used as a type.  Besides defining which variables go into the structure, other information on what can be done with the structure are also defined such as:

Structures don't store any run time type information.  This means that there can not static static variables or virtual expressions.  It is also not possible to check the type at run time.

Variables

"var" {VarItems}
VarItems = Identifier ":" TypeDecleration ["=" ConstValue]

The var block specifies all the fields in a structured type.  Variables are always accessible by the outside world.  When generating the memory locations, the order of decleration is observed.  For all the decleration possiblities, see Type declerations

example:

var x: int
    y: int = 10

Properties

"prop" {identifier ":" TypeDecl 
["read" [ExpDecleration | identifier |"var" MemLoc]]
["write" [ExpDecleration | identifier |"var" MemLoc]]
}

The property block identifies a list of fields that are accessible to the outside world.  However, with properties you can control access to the value through the definition of a reader and writer.  These can be an expression or a variable.  If the variable is declared inside the property, it is local to the object and the outside world can not access the value.  You can also use a previously declared variable to specify the read or write value.

If  you use an expression, the result type of the read part needs to be the same type as that of the property and it should not have any parameters.  The writer needs 1 parameter of the same type as the property and no result value.

When generating the memory locations, the order of declarition is observed.  This means that a property with 1 or 2 internal memory declerations will store these variables in the order that the property appeared with the memloc that was declared first, at the first possition.

For instance:

struct x =
 var a: int
 prop b: int read var c: int
 var d: int
;

will be converted to

struct x =
 var a: int
     c: int
     d: int
;

some examples:

struct x =
 prop a: int read exp GetVal: int write var b: int
;

exp x.GetVal =
return(b)
;

struct x =
 var b: int
 prop a: int write b
;

Expression declerations


"exp" { ExpDefinition} 
ExpDefinition = Identifier ["("[ MemLoc {","MemLoc} ")" ] [":" TypeDecleration]
["in" identifier[":" identifier] {", " identifier[":" identifier]} ]
["out" identifier[":" identifier] {", " identifier[":"identifier]} ]
["inline"] ["static"] ["extern" [identifier] ] ["abstract" | "override"|
"virtual"] ["default"]
[ExpImplementation]
ExpImplemention = "=" {Statement} ";"

An expression is the equivalent of a function.  It is called expression cause unlike functions, expressions can contain a form of regular expressions (inteface statements).  For more on this, see Message statements

Just as functions in most other languages, an expression can have parameters and/or a result type.  For parameters by value of complex types, a local copy will be made using the assign operator if it is defined, otherwise by simply making a copy of the data.

The special keywords "in" and "out" allow you to specify to/from which message handlers the expression can send and or receive data.  This needs to be specified if you want to use message statements in the code.  A message handler can be specified as a normal var decleration (identifier: typedecleration) or by specifying the name of the global variable (identifier).

The keyword 'Inline' specifies that whenever a call is made to the expression, the body should be copied inside the caller instead of making a call.

The static keyword defines that the expression can be called from the type.  Static expressions can't access field data.

Extern expressions are only allowed in libraries and processes.  Extern expressions can be called from outside the library, they are exported.  Possibly, an extra name can be specified in case the name of the function in the library is different than that of expression.  This allows you to use different names in the object than the actual names found in the library and it also allows you to link to case sensitive libraries such as the ones produced by C compilers.

Abstract, override and virtual are only allowed in complex types (spaces and stores).  Abstract expressions don't have code associated with them but need to be implemented by a descending object.  If you want to allow an exprssion to be reimplemented by a descendent, you use the virtual keyword.  Expressions that are reimplemented need to use the override keyword.  Virtual and abstract expressions will make certain that the correct implementation of the expression is used for the caller.  If you reimplement an expression without initially declaring it abstract or virtual in the base type, you will always use the expression that is located in the type of the variable, not in the data's real type.

The default keyword can only be used with constructors and destructors.  For more info on this, see constructors or destructors.

After an expression decleration, you can always give the implementation of it.

Overloaded operators

OperatorOverlaoder = {"'"Operator"'" ExpDefinition}
Operator =[ "*" | "/" | "%" |"&"
|"+" | "-" | "|" | "~"
|"=="| "<" |"<=" | ">" | ">=" | "<>"
|"=" | "+="|"-=" | "*="| "/=" |"%=" | "<<" | ">>" | "<<-" | "->>" ]

Structured types can have operator overloaders.  They allow you to (re)define the behaviour of an operator on an object. 

Notes:
Examples:

//create an operator overloader so we can assign a char to the object
operator '=' DoAssign(aVal: char)
//a multiply operator for a vector type that is reversed so we can
//write aint * avector
operator '*' DoMul(aVal: int, aVector: sVector) static

Constructors

Constructor = "constructor" {ExpDefinition}

A constructor is defined just as a normal expression. If an object has no constructors, it can not be created dynamically. A constructor can be defined as default, which indicates that this is the constructor that should be used for creating the object in situations were the object is being created without a constructor (such as a parameter by value or a field of a structured type).  There can only be 1 default constructor per type and it can not have any parameters.  Other constructors can have as many parameters as you want.

Notes:
Example:

constructor create = ; default

Destructors

Detructor  = "destructor" {ExpDefinition}

A destructor is defined just as a normal expression. If an object has no desstructors, it can not be destroyed.  A desstructor can be defined as default, which indicates that this is the desstructor that should be used for destroying the object in situations were the object is being destroyed without a destructor (such as a parameter by value or a field of a structured type).  There can only be 1 default destructor per type and it can not have any parameters.  Other destructors can have as many parameters as you want.

example:
destructor destroy = ; default

Send to Message handler

"message" { identifier ["read"ExpDefinition] ["write" ExpDefinition] ["is" ExpDefinition]}

For more info on message principles, see messages.

With message receive definitions you can define which objects can be sent to the defining object.  You need to specify the object type that we can send to, the read, write and verify function.

The read function will get a value from the defining object.  The write function will write the value.  The verify function will check if the requested type is available

For this to work, each function needs to have a set number of parameters and a specific return value.

Example:

suppose we want to write and/or read the data from an object to a Container objects.  To do this, we can define the object as follows:

space sContainer =
entity pointer //stores refs to anything
store stLinkedList //store the data as a list
var CurPos: int = 0 //stores at which position we are currently
Next DoNext = CurPos += 1;
;
space sElement =
 var Val1: string    
 message string read ReadFromContainer(aContainer: sContainer&): sElement static
write WriteToContainer(aContainer: sContainer&) inline       
is IssElement(aContainer: sContainer&): bool static inline

exp sElement.ReadFromContainer(aContainer: sContainer)
=
return(sElement^(aContainer@CurPos.entity)^)
//note: don't advance, this is done by the GetNext of the container object
;

exp sElement.WriteToContainer(aContainer: sContainer)
=
   //add ourselves to the list
aContainer << pointer(self)
;

exp sElement.IsElement(aContainer: sContainer): bool
=
//check if the object at the current position is of our type
   [<-aContainer@CurPos is sElement-> return(true)
<--> return(false)
]
;

Union

"union" identifier "=" { Var | ExpDeclerations } ";" 

A union allows you to define variables that occupy the same memory location.  In additionn, you can define expressions that are local to the object.  The compiler will allocate enough memory to accomodate the largest variable in the union.  Since memory is shared between the variables, writing a value to one variable also changes the other variables.

To select a variable within the union, use the "." selector.

example:

union uIntAndFloat =
  anInt: int
  aFloat: float
;

aVal: uIntAndFloat
aVal.anInt = 5
aVal.aFloat = 5.5     //value of anInt is
overwritten

Complex types

Complex types are types that contain run time type information.  There are 2 variations: spaces and stores.  Since complex types have run time information, they can contain static fields and virtual expressions.  Complex types can also inherit from one or more parents (multiple inheritence).  All parents must be of the same type as the current type.  Or in other words, a space can only inherit from other spaces and a store only from other stores.

You can also check if a complex object is of a specific type or if the type is in the inheritence tree of the object by using the '=' and 'is' operators.

Spaces


"space" Identifier ["(" [ Identifier {, Identifier} ] ")"] "="
{SimpleVar
|PropDef
|ExpDeclerations
|OperatorDefs
|constructorDefs
|DestructorDefs
|MessageSendDefs
|MessageReceiveDefs
|"entity" TypeDecleration
|"storage" Identifier
|"Next" ExpDefinition
|"space" Designator
}";"

A space is a complex type that expands the struct type (a struct type with run time type information).  A space can also receive objects that are send to it through the messageing system.  Furthermore, a space can operate as a container for other object of a specific type.

The following capabilities can be defined in a space:

To define a space as a container, you use the "entity" and "storage" sections.  Under "entity" you  specify a type decleration of the object that you want the store.  The "storage" keyword is used to let the system know how it should manage this data.  This has to be the name of a store.  A space doesn't have to be a container.  If the entity and storage keyword are not suppplied, it is impossible to store data in the space.  

If the first parent space defines the sections, descendants will inherit these settings.  They can not be overwritten. All secundary parents can retain their container definitions.  This means that a space can actually have multiple containers, one of it's own and one for ech of the secundary parents which can only be reached by casting to the parent type.

Normally, an entity always keeps a reference to the space that it owns, but if the entity is a structured type that already contains a reference to it's owner, you don't need to store this information  2 times, therefor, you can use the keyword 'space' to indicate which field of the entity has the ref to the owner.

There are special operators provided to add and remove data from a space.  There is also a general way to loop through the data.

When data is added or removed to/from a space, this is done through the store that it defined.  Sometimes you need to call some extra code, specific for this space during these operations.  This can be done be implementing an operator overloader (for the add and remove operators) with as parameter, a reference to the space entity.  In the case of an add, this expression will be called after the actual operation, for the remove, it will be called before the data is removed from the space.


examples:

//a simple string type
space sSimpleString =
 entity char
 storage stArray  //this is a store defined somewhere else and represents an array
;

//an example using space

space sOwner; //forward declare
struct sChild =
var Owner: sOwner^ //a ref to the owning object
;
space sOwner =
entity sChild //the object type we store
store stLinkedList //how we store it
space Owner //where can we find the owner in the object type we store
;

//an example with an operator overloader for the add and remove in the space
space sOwner =
entity sChild
store stLinkedlist
operator '<<' AddElement(aValue: sOwner.entity) = aValue.Owner = self;
operator '>>' RemoveElement(aValue: sOnwer.entity) = aValue.Owner = nil;
;

Message receiving

"type" {identifier ["read" ExpDefinition] ["write" ExpDefinition] ["is" ExpDefinition]}

For more info on messaging principles, see Messages.

With message receiving definitions you can define which objects the can be sent to the defining object.  You need to specify the object type that we can handle, the read, write and verify function.

The read function will return a value of the type this message is suppose to handle.  The write function will write the value from the defining object to the message object.  The verify function will check if the message handler can read the value(s) as the specfied type.

For this to work, each function needs to have a set number of parameters and a specific return value.

Next

The Next section of a message is used to define the expression that needs to be called whenever a new element needs to be fetched from the message handler.  This is done automatically each time that a read of the data is performed on the object and compared to another value or stored in a variable.  The first time however, this expression needs to be called manually.  Most of the time this is done in the constructor or some other location.  For instance, the sFile space will issue a manual 'GetNext' each time a file is opened.
This means that Gene always needs 1 look ahead value in it's buffer.  However, in some situations, this is not possible.  For instance, when you are reading from the screen, you can not have a look ahead, you can only read what is wanted at that moment.  This can be solved by handling the actual reading in the 'read' parts of the interface definitions and performing some other task in the 'GetNext' or leaving it empty.
A simple example of a GetNext expression would be one that sets the next character index to be read from the string.  This index can than be used by the other interface parts to get and compare the char.

Examples:

Suppose we reimplement a string type so that we can send and read integer values to/from it.  Notice the
GetNext expression.  This is used to scan the string.

space sIntListString(string) =
 var CurStart: int = 0
     CurEnd: int = -1  //so that we have a proper first init
 type int read ReadFromString: int
          write WriteToString(aVal: int^) inline
          is IsInt: bool inline
 Next DoNext
 constructor create default = DoNext;
;

exp sIntListString.ReadFromString: int
=
   //get the substring out into a temp string, convert that into an integer
   iTemp: string
   iTemp = self.SubString(CurStart, CurEnd)
   return(Convert.StrToInt(iTemp))
;

exp sIntListString.WriteToString(aVal: int^)
=
   //convert the int to a string, add it to us
   iTemp: string
   iTemp = convert.IntToStr(aVal^)
   self += iTemp + " "
;

exp sIntListString.IsElement(aString: string): bool
=
   //There is an integer waiting if the start is different from the end
   return((CurEnd - CurStart)> 0)
;

exp sIntListString.DoNext
=
   //find next space, check if we found  an alpha numeric value, if so,
//set start and end to end, otherwise, it was a number so leave start 
   iNeedEqual: bool
   [<-(CurEnd + 1) < Self.NrRecords->
      iNeedEqual = false
      iChar: char
      CurEnd += 1
      iChar = Self@CurEnd
      {<-CurEnd >= Self.NrRecords->
       >-iChar == " "-> break
       <-->
         [<-iChar < "0"|| iChar > "9" -> iNeedEqual = true]
         CurEnd += 1
         iChar = Self.@CurEnd
      }
    <--> iNeedEqual = true
   ]
   [<-iNeedEqual-> CurStart = CurEnd ]
;

Stores

"store" Identifier ["(" [Identifier {, Identifier} ] ")"] "="
{ SimpleVar
| PropDef
| ExpDeclerations
| OperatorDefs
| constructorDefs
| DestructorDefs
| InterfaceSendDefs
| "Loop" ExpDefinition
| "space" {MemLoc}
| "entity" {MemLoc}
| "first" ["read" ExpDefinition] ["write" ExpDefinition]
| "last" ["read" ExpDefinition] ["write" ExpDefinition]
| "next" ["read" ExpDefinition] ["write" ExpDefinition]
| "prev" ["read" ExpDefinition] ["write" ExpDefinition]
| "NrRecords" ["read" ExpDefinition] ["write" ExpDefinition]
| "pos" ["read" ExpDefinition] ["@" ExpDefinition] ["write" ExpDefinition]
}
";"

A store is a complex type that is used to define data storage systems. It has the same capabilities as a structure with the added benifit of inheritence.  A store is used by a space to define how its data can be accessed, what the possibilities are and what is needed for this type of storage mecanisme.  A store defines the fields needed in the space and in each record, how the space should loop through the data, how to get to and/or set the first, last, next and previous record, how to get and/or set the total number of records in the space and how to get, set and retrieve a record at a specific position within the space.

Besides these special properties, a store can also define how it is supposed to add and/or remove a record from it's internal data by implementing the '<<' and '>>' operators.

The loop expression can not be static or virtual.  It expects 1 parameter: an integer which is the total size of 1 record.  It has no return value.  The 'inherited' keyword is overloaded in the loop expression.  It expects 1 parameter: a pointer type that should ref to the current entity to be processed in the loop.  The 'inherited' keyword in the loop expression indicates where execution needs to be handed over to the system to perform the inner loop code.

The get first and last expressions expect an int as parameter, representing the size of a record and return a pointer, which is the record.  The set first and last expect an int and pointer as parameters in that order, meaning the size of a record an the record which contains as it's first fields, the fields defined in the entity section.  No return value is expected.

The get next and prev expressions expect an int and pointer as parameters in that order and return a pointer. The first parameter indicates the size of a record, the second, gives the previous record.  The set next and prev add 1 more parameter as pointer, the new record.

The Get NrRecords expression has 1 parameter: the size of a record.  It returns an int representing the number of records in the store.  The Set NrRecords expressions has 2 parameters: an int representing the size of a record and an int representing the new number of records.  There should be no return value.  The manner on how a decrease in the record count is handled is not defined and is therefor left over to be defined by the store

The get at Pos expression expects an int representing the position and an int representing the record size in that order.  It returns a pointer representing the record found at the specified position.  The GetPos (read) expression expects a pointer representing the record we want to know the position of and an integer representing the size of a record.  It returns an integer, representing the position of the record.  The write position expression gets an integer represinting the old pos, an int represinting the size and an int represting the new position in that order.  It does not return a value.

All the first, last, next, prev, nrRecords and pos expressions can be static or virtual but don't have to be.

You shouldn't normally have to write store objects unless you are designing core libraries.  Gene contains a set of predefined store objects that you can use.  These are located in the 'BaseStores' unit.  You can use stores like linked list, stack, queue, array,... which behave just like normally.  For more information on the specific stores that are available, check the library documentation (at time of writing not ready yet).

Type declerations

Type decleration = [ [Identifier | designator "." "entity"]   [ [ "$" | "&" {"^"}]  
| "." Constructor
| "@" Integer
]
| Struct
| Union
| Space
| Store
| ExpDecleration
]

Memory locations need to have a type decleration.  This can be:
Notes:
Examples:

//decleration of a variable of type int, initialized to 0
a: int = 0

//array of 5 elements of type int
a: int@5

//pointer to type int (or variable length array), initialized to nil
a: int^ = nil

//pointer to type int that will automatically be initialized to nil and
//will be destroyed once it gets out of scope
a: int$

//nested definition of a space (structured object)
a: space x =
     entity int
     storage stLinkedList
    ;

//definition of a struct object and variable that initializes the fields
struct x =
  a: int
  b: int = 0
  constructor create = ; default
  destructor destroy = ; default
;
a: x.Create(\ a = 2, b = 3)

Rename

"rename" identifier "=" [TypeDecleration | ExpDecleration ] ";"

Use the rename keyword to assign a symbol name to a type decleration.  This can be used to create shorter names for typedeclerations, to make the code more readable or more typesafe and to declare pointer types to expressions.  Gene treads a renamed type as the same type, so it not type safe except for messages where a renamed type is seen as a new one.  This can also be used in the messaging system to identify types that have a different logical meaning but have a similar physical representation.  In this case, one is usually a subset of the other.  For instance you can have a string type to handle normal strings and rIntAsString which is a rename of the string type to handle strings that can be converted to integer values such as '123'.

examples:

rename rWidth = int;
rename rIntAsString = string;
rename rStringInitToJan = String.Create('Jan');
rename rSimpleExp = exp (aValue: int) virtual;

note:
if you have 2 declares as:

aWidth: rWidth
aInt: int = 1
aWidth = aInt  //will work
aWidth = rWidth(aInt)  //using a cast will also work but is not needed

Global variables and constants

Constant = "const" {Identifier "=" ConstValue }";"
ConstValue = ["nil" | integer | extended | String | Bool | Char]
Variable = "var" { Identifier ":" TypeDecleration } ";"

Globals can be accessed anywhere in the code of a unit that includes the unit containing the definition of the global.  Globals can be constant or variable.  Constant globals must be assigned a value during decleration and can't have a type specifier.  They can only be of a base type, so integers, boolean, extended, characters, constant strings or the nil value.  Their value can't be changed during the execution of the application.  Variables must have a type definition.  This can be any type that is known at the location where the variable is defined.  They can have an initial value assigned and it can change during the live of the application.

A global variable of a complex type is created properly with either the default constructor, the one specified at the decleration or none if there is no default constructor and the default fieldvalues are assigned before any code is executed.

examples:

const 
gInt = 1
gFloat = 1.1
gString = 'This is a test'
gBool = false
gEmpty = nil
gChar = "t"
;
var
gString: string.create('initValue')
gInt: int = 0
gFloat: float
gPerson: sPerson.Create(\ Name = 'Jan Bogaerts', Sex = Sex_Male)
;

Processes and Libraries

"process" identifier ["(" Identifier ")"] 
{ "OS" ["linux" | "win32" | "win32consolse"]
| ["platform" "x86"]
} "="
{ "entry" ExpDefinition
| SimpleVar
| ExpDeclerations}
}
";"

"lib" identifier ["(" Identifier ")"]
{"OS" ["linux" | "win32" | "win32consolse"]
| ["platform" "x86"]
} "="
{ "entry" ExpDefinition
| "return" ExpDefinition
| ExpDeclerations
|constructorDefs
|DestructorDefs}
}
";"

A process object identifies an application that needs to be generated.  A library object identifies an object file that is used by an application at run time and that is not contained by that application (in other words dll's or os files).

A Gene project can have multiple processes and or libraries as output.  An output is generated for each process and or library that is contained in the project unit.  Processes defined in other units are only accessible through inheritence.  Libraries declared in other units can be used, but no output will be generated for them.  This allows you to define common run time libraries in seperate units and include those units to use these libraries.

Unlike libraries, processes can inherite from other processes.  Descending processes can access the variables and expressions of the parent process.  Variables are always static and expressions can not be abstract or virtual.

The entry point is used to define the start of the appliction or library.  A process must always have one but a library doesn't.  A library can also define an exit point if it wants to.  The definition of the entry/exit point is fixed and determined by the Os.  Current valid definitions are:

Type
Os
Definition
Process
Win32
main(CurIntance: THandle, PrevInstance: THandle, CommandStr: char^, CmdShow: int): int

Win32Console
main(Argc: int, Argv: char^^): int

Linux
main(Argc: int, Argv: char^^): int
Lib
Win32
main(hinstDLL: THandle, fdwReason: dword, lpvReserved: pointer): bool (for entry and exit point)

A process can have variables and expressions.  These variables and expressions are only available inside the process.  A library can't have variables, only expressions.

Each library and process needs to be assigned to an Os and and possibly platform.  Currently, only linux and windows (normal and console) are supported as Os and for the platform, only x86 is available.  This may be expanded in the future.  This information can be used to write platform specific code through the use of the interpreter.

Example:

#[CurrentProcess.Os
#<-Linux-> Kernel32.CloseHandle(fHandle)
#<-Win32-> CloseFile(fFile)
#<--> #errors << StrErrorLine.Create(f: Unit = Name, Line = CurLineNr, Col = CurColNr,
Text = 'Platform not supported')
#]

Libraries can also have constructors and destructores.  They are used to dynamically load/unload a dll at run time.  The constructor call of a library  has 2 extra parts besides the parameter list: the name of the library, which always has to be string and a list of assignments for binding function names to expressions that have a different name than the expressions themselves.

Note: The code declared in a constructor, destructor of a library is situated in the calling process while the code for the entry/exit points is in the library itself.

To load libraries in a process, you need to declare variables of type lib.  If the variable is not a pointer, the library will be loaded statically at start up of each application in the project file.  If the variable is a pointer, the library will only be loaded when the variable is created through one of its constructors.  Expressions defined as extern are the exported functions of the library file.  If no name is given after the 'extern' keyword, the name of the expression will be used to load the function, otherwise the given name will be used.  If names are supplied in the constructor of the lib, these names will be used.  To unload a library, call the destructor of the variable.

example: 

process foo Os linux =
entry main(Argc: int, Argv: char^^): int
var IntValue: int
;
lib foolib Os linux platform x86 =
exp dox
doa(aValue: int) extern
dob extern '__dob'
constructor create
destructor destroy
;
var StaticLib: fooLib
RunTimeLoaded: fooLib^
;
RunTimeLoaded.Create(\ Doa = '__doa')

Automations

An automation defines a way to generate code.  they can be compared with C++ templates but with more possibilities.  They can also be used to create C++ Defines.  Their primary puspose however is to allow you to create your own meta types that you can use later on in the code.  they can be used in various situations such as generating algorithms, making OS independent implementations, GUI design, help systems,...

For instance, suppose you have the spaces sScreen, sKeyboard and sDataBaseConnection wich, for some reason need to be singletons.  You could make a base space that implements the code to verify that there are no 2 objects created and inherit from that.  But because Gene does not allow multiple inheritence, this can become tricky sometimes.  Instead, you could define an automation 'SpaceSingleton' that inserts the code into space definitions.  Then, instead of creating space types, you can create SpaceSingleton types, which are spaces with the added functionality of verifying that they are only created one time.

An automation has 3 characteristics:

An automation takes an input definition of an object type and transforms this into something new.  The type that it uses as an input is predetermined through the design of the automation.  This can be: a space, struct, store, enum, expression, var (global), rename, union or property.  The end result can be an expansion of an existing object type or it can be something completly new.  This depends on the design of the automation.

Some automations need extra parameters for them to be able to generate properly or to give extra options.  This is done through chapters.  Besides the default types such as bool, int, float, ... chapters can also be of special types such as identifiers, var, stringlists,... (see using automations for more info).  The best way to think of and use a chapter is by considering the following: If a struct would be an automation (instead of a predefined type), it's chapters would be: var, prop, exp, operator, constructor, destructor, message.

Using automations


AutomationName ["(" identifier ")" ] { "\" AutomationName ["(" identifier ")" ] } identifier [ typespecific ]
{ChapterName Value}
";"

There are 4 importent things you need to know when using an automation:

To use an automation, you define an object with as type name, the name of the automation.  For instance, suppose you have a space 'X' and you would like to make this space a singleton by using the SpaceSingleton automation, you would delcare it as SpaceSingleton X instead of Space x.   Next, you give the values for each chapter you want (have) to implement.

You can let an automation implementation inherit from another automation implementation.  In that case, the chapter values are copied to the descendent (if they are not defined as 'override').  When a chapter is redefined and it is not a list type, the values will be overwritten.   When it is a list type that doesn't have the override specifier, the items will be added to the back of the list.

Note: for spaces, stores and structs there are actually 2 ways you can inherit an automation implementation: through the automation name or through the normal inheritence declaration (if this is an automation).  the difference between the 2 is that in the first instance, only the chapter values are inherited while in the second instance, the 'inherited' value is also set.

For instance:

singletonSpace(Base) descendant =;  In this case, only the chapter values will be inherited

or singletonSpace descendant(Base) = ; In this case, the chapter values will be inherited and the descendant object can also inherit from base (depending on how the automation generates it's code)

It is also possible to cascade multiple automations.  For instance, suppose you have a struct that you want to observe and that needs to be a singleton.  This can be done by putting the 2 automation calls after each other.  Note: the order in which you put them is very important: the output of the inner automation will be the input of the outer automation.  Each automation can inherit and receive it's own chapter values.

example:
suppose we have a struct with a variable.  We want this variable to be observable (that is, when it's value changes, other object are informed, and some code can be performed for them).  We could use a VarObserver automation.  This automation has as chapters 'ObserveBefore' and 'ObserverAfter' which both have a default value of false to let the automation know if code is called before and/or after the value is changed.
This could be implemented as follows:

//original definition
struct x =
var y: int
;
//with observer
struct x =
varObserver y: int ObserveBefore true; //we only supply the ObserveBefore, we leave the ObserveAfter as its default, so false
;

suppose we want to observe this intire struct and it needs to be a singleton
This can be written as:

//original definition
struct x =
var y: int
;
//with observer
SingletonStruct Observedstruct x =
var y: int
;

The messaging system

Gene allows you to send and receive data through a messaging system.  This allows you to write very compact and readable code.  It is based on the principle that you only have to specify 'what' and not 'how', for this is always the same for the same type of data and message handler.

For instance, if you want to write a string to a file, you can always do this in the same manner, by calling the 'write' function of the OS or of an object and passing the string as a parameter, with possibly some extra paramters.  However, if you use the same way of writing to a file for the intire expression, you should only have to specify 1 time 'how' it needs to do this, from then on, you only need to specify 'what' needs to be written.

This also applies for reading data from an input.  Besides specifiying how the data is read in, the system also needs to know if it can be read.  From than on, read operations can always be expressed as:

Gene does this by allowing an expression to be assigned to one or more message handlers as input and/or output.  Whenever there is a variable or literal found in the expression code, it will check the type of the variable/literal and look up the appropriate handler for the correct direction (input/output).   The following matrix explains the actions taken:

Statement
Input
Ouput
constant
  • check if type can be found in input
  • read value and compare to constant
  • let interface get the next value
write value
variable
  • check if type can be found in input
  • read value and store in variable
  • let interface get the next value
write value
calculation
  • check if type can be found in input
  • read value and compare to calculation
  • let interface get the next value
write value
name of type
check if type can be found in input
let interface get the next value

If a conditional statement (loop or option) doesn't use a boolean expression as it's condition, it will also use the interface system to compare/retrieve the value.  Unlike normal statements, conditional statements will always use the input of an interface and never the output.  The various condition statements are handled in the same  manner as the normal statements.  That is, a constant and caluclation will be compared, a designator will store the value and an IdType will check if the type can be found in the input.  It is also possible to let the condition statement begin with a boolean operator, in that case the value in the input buffer will be compared with the expression after the boolean operator using that operator.  Wether the next value is retrieved if the condition evaluates to true depends on wether the condition block is closed wth a '->' which will get the next item or a '-<' wich will not get the next item.

The interpreter is also able to handle messages.  In fact, the interpreter sees all 'non interprete' statements as messages to be sent to it's output (the parser).  This allows you to put the content of an interprete variable in the output code.

Statements

Gene uses statements to perform actions.  These statements can be defined at 2 different levels of execution:
The interprete statements form a subset of the expression statements.  The following matrix defines which statements can be used when:

Statement Inteprete
Execute
Variable decleration only base types
yes
Assignments yes yes
Copy statements no
yes
Shift statements yes yes
Message statements yes
yes
return statements yes yes
Options and loops yes yes
Break and Continue yes yes
Inherited
no
yes
Constructor call no
yes
Destructor call no
yes
Exception handling no
yes

Variables

Identifier ":" TypeDecleration

Variables declared inside an expression are local to the block in wich they are defined.  If the variable is a complex type (no pointer), and there is no constructor supplied, the default constructor is used, otherwise the supplied constructor will be used.  When the variables gets out of scope or an error is raised, the default destructor will be called.

If the variable is an owned pointer, it will be initialized to nil.  When the variable is not nil at the time it gets out of scope or an error is raised, it will be freed.  If it was a complex type, the default destructor will be called.

If the variable is declared by reference, it will create and destroy the object .

If there is a default value supplied in the type decleration, this will be used.  Note, when generating to C and the C compiler compiles as C++, variables declared inside an option or loop don't allow initilization.  This is because the options and loops are constructed using goto's.

Assignments

Condition ["=" | "+=" | "-=" | "/=" | "*=" | "%=" ] Condition

An assignment copies the value from the right side to the left side.  Depending on the assignment operator, it will:
For simple types, structures and unions, both the left and right type need to be of the same type.  If it is a structure and it has an operator implemented for the type of the of the right part, the operator overloader will be used, otherwise, the operation is illegal. 
Note that assignments can not be sequenced.  It is only allowed to write a single assignemt per statement.

examples:

a: int
b: int
a = b
c: char
a = c //error
a = int(c) //ok
aString: string
aString = 'test' //operator overloader will be used
aString += ' ' //operator overloader will be used, ' ' will be added at end of the string

Shift statements

Condition ["<<" | ">>" ] Condition

A shift statement will shift the bits of the left part to the left or right by the number of items defined in the right part.  Shifts can only be performed on base types.  When the left and right parts ar base types, a shift instruction will be assumed.  Spaces reimplement the shift operators as the copy operator.

examples:

a: int
a << 5 //will shift the bits in a 5 positions to the left

Copy and move statements

Condition ["<<" | "<<-" | "->>" | ">>" ] Condition

This statement is used to add and/or remove data from a space.  Depending on the type of both sides of the condition, a different action is taken.
Left
Right
<<
<<-
->>
>>
space 1
space 2
add data from space2 to space1
add data from space2 to space1 and remove data from space 2
add data from space1 to space2 and remove data from space1
add data from space1 to space2
space
entity of space
add new element to left, copy data from right



space
entity of space constructor
add new object to left reffed in right, perform constructor



space
var
add new element to left, copy data from right



space
constructor
add new element to left, store ref in right perform constructor



space
nil


remove the data from the space

entity of space
space



add new element to right, copy data from left
entity of space constructor
space



add new object to right reffed in left, perform constructor
var
space



add new element to right, copy data from left
constructor
space



add new element to right, store ref in left perform constructor
nil
space

remove the data from the space



examples:

//suppose we have the following space definitions:
space sStringList =
storage stLinkedList
entity string
;
space sIntList =
storage stLinkedList
entity int
;

var iStrings1: sStringList
iStrings2: sStringList
iStrEnt: sStringList.Entity //entity of space sSTringList
iInts: sIntList
iStrVal: string
iIntVal: int
;

//add element with a constructor
iStrings1 << string.Create('an initial value')
//add element through the entity constructor, after the operation,
//iStrEnt will point to the element that was added
iStrings1 << iStrEnt.Create
//copy the values from list 1 to list 2
iStrings2 << iStrings1
//empty list 1
iStrings >> nil
//add integer to list
5 >> iIntEnt
iIntVal = 1
iIntVal >> iIntEnt

Expression call

condition

An expression call is used to invoke an expression.  The call is made by writing the location of the expression (through a designator or the result of another function call or a space selector,..) and supplying any possible parameter values and message handlers.

If the expression being called has parameters, the values to these parameters have to be passed between brackets, in the same order as they were defined, seperated with a comma.  Only parameters that have a default value can be ommited and only at the end of the parameter list withoug gaps.  if the expression has no parameters, the brackets can be ommited.

Parameters are normally passed by value.  To pass complex types by value, Gene creates temporary objects and passes these on to the stack.  The complex type needs to have an assign operator overloaded for the type that is passed as value or the the parameter and the value need to be of the exact same type so that a memcopy can be performed.  When the call is completed the temporary object will be destroyed and the default destructor will be called.

If the expression had input and/or output interfaces assigned, you should write an interface selector.  This is performed with the '@' operator followed by the name of the interface if it was a global or a factor (designator or condition between brackets) resulting in the same type as one of the interfaces defined in the expression. 

If there is only 1 interface and it is not a variable, you can ommit the interface selector.  If the expression where the call is written in, also has interfaces, and you ommit the interface selector (when there are multiple interfaces defined or a single interface as parameter in the expression being called), the same interface will be used as the interface used to call the owning expression.

examples:

space x =
var StrVal: string
exp DoSomething
dox in aString: string //read from a string
doy(aVal: string) out screen //write to THE screen (screen is a global parameter)
doz out aString: string, screen //write to a string and a screen
;

exp x.DoSomething =
iStr: string
dox@iStr //we have to provide an interface object, cause it is defined as a variable
//a temp var will be created and the assign operator will be called
//to copy the data of the string
doy(iStr) //don't have to provide interface, the compiler knows you mean the screen
//when the call is completed, the temp var is destroyed.
;
exp x.dox =
doz //we don't have to provide an interface, the compiler will automatically use
//the interface of dox doz will put 'test' in the interface string 'test'
//we want to read test out of the interface
;
exp x.doz =
'test' //put test in the interface
;
exp x.doy(aVal: string) =
//write the value that was passed as a parameter
aVal //ask doz to write 'test' to the screen, we don't have to provide an interface,
//the compiler will use the screen
doz
;

Message statements

condition

A message statement consists out of a single variable, constant or calculation.  It can only be used in expressions when they have interfaces assigned to them.  The type of the condition needs to be implemented by each interface object.  Depending on the definition in the expression, read or write code will be generated for the inteface statement.

examples:

//suppose we have a definition of:
space a =
var b: int
exp dox in string out file
;
exp a.dox =
aVal:string = 'my age is'
b = 1
aVal ' ' b ', but could also be ' b+2
;

This could have as output in the string  and file(depending on how int is implemented in string): 'my age is 1, but could also be 3'

return

"return" ["(" Condition ")"]

Use the return statement to exit from the current function back to the calling routine, optionally returning a value.

examples:

return
return(false)

Options

"[" CodeItems {CodeItems} "]"

Use an option to perform a conditional statement.  The option can be compared with the if elseif and case statement of traditional languages, combined.  Execution flow is controlled through the conditionblocks.  Code following the first conditionblock that is evaluated to true will be executed untill the first conditionblock that isn't defined with a fall through or the end of the option.  If the first statement of an option is not a conditionblock, it should be a condition (variable, or calculation), in which case the option is considered to be a case statement.  In this situation, the next statement should be a conditionblock and all condition blocks in the option should return the same type as the first statement.  This means that case statements in Gene are not limited to the evaluation of integer values.

examples:

//simple if else if structure
[<-a == 1-> b = 2
<-c == 2-> d = 3
]
//case statement
[a
<-1-> dox
<-2-> doy
]
//another case statement
[aString
<-'test'-> dox
<-'tester'-> doy
]
//an if statement with a fall through
[<-a == 1-> dox //if a == 1 then dox and doy will be executed
>-a == 2-> doy //if a == 2 only doy will be executed
]

Conditionblocks

[ ">-" | "<-" ] Condition ["\" Condition ["\" Condition]] ["->" | "-<" ]

A conditonblock controls the execution flow for options and loops.  If the condition inside the block evaluates to true, the code that follows will be executed untill the next condition block that is defined without fall through or the end of the option /loop.

If the conditionblock is used in a loop, it can have multiple sections.  If this is the case, the conditionblock should be the first statement in the loop and there can't be any other conditionblocks.  There can't be interface calls if the conditionblock contains multiple sections.  See loops for more info on the meaning of the various combinations.

If the condition inside the conditionblock is not a boolean calculation (does not contain <>, ==, >=, > , <=, <) or the left part of the boolean condition is missing, the interface system will be used.  Interface calls performed by the condition block are always performed as input. If the exression has output interfaces defined, the usage of the condition blocks inside an option/loop allows  the implementation of input - output expressions.  Depending on the type of the condition, different actions will be performed:

  • a constant or calculation:  Check if the type can be read from the interface, get the value, compare the value with the constant
  • a type name: check if the type can be read from the interface.
  • a variable: check if the type can be read from the interface, get the value and store it in the variable
  • a boolean calculation with the left part missing: check if the type can be read from the interface, read the value, do the comparison with the calculation
  • an expression call: get the first interface statement from the expression, if this is a loop or option, use all the interface calls in the condition blocks (the loop or option can only contain interface calls in the conditionblocks, otherwise it is illegal), read the type and value and compare it with one of possible interface calls.  If one matches, call the expression. 

After the conditionblock, before any other code is executed, the interface will be asked to advance by calling it's 'next' expression.  This will not be performed if the conditionblock is closed with a '-<'.  If the condition block contained an interface call through an expression, there will never be a next call.  When the expression is called, the location of the interface remainded the same so that the interface call inside the expression can be handled correctly.

examples:

aval: string
//if there is a string in the input, store it in aVal,
//don't advance and call 'DoSomething'
[<-aVal-<
doSomething(aVal)]
//if there is a string in the input, store it in aVal,
//goto next in interface and call 'DoSomething'
[<-aVal-> doSomething(aVal)]
//if 'test' is in the interface, goto next in interface, call 'DoSomething'
[<-'test'-> doSomething]
//if there is an int in the interface and it is bigger than 2,
//goto next in interface and call 'DoSomething'
[<- >2 -> DoSomething]
//an interface statement through an expression, the option will be executed
//if the interface currnently contains
//'x', 'y' or 'z'. Before DoSomething is called, dox will be called
[<-dox-> DoSomething]

exp dox =
a: int = 0
{<-'x'-> doa
<-'y'-> dob
<-'z'-> doc
}
dod
;

Loops

"{" CodeItems {CodeItems} "}"
 

Use a loop to perform a  repetion of conditional statements.  The loop can be compared with the while, repeat and for statement of traditional languages combined.  Execution flow is controlled through the conditionblocks wich may have multiple sections.  Depending on the number of sections and the location of the conditionblock, different types of execution flow can be achieved:

  • If the first statement is a condition (variable or calculation) followed by one or more conditionblocks with a single section and no boolean calculation and where the last conditionblock is possibly empty, a case loop will be used.  The loop will be performed for as long as the first condition has one of the values defined in the conditionblocks.  If the last conditionblock is empty, the loop will be performed infinitly (use a break to stop the loop).  The code following the empty block will be performed if the value of the first condition doesn't match any of the other conditionblocks.
  • If the first statement is a conditionblock with a single section, a normal loop will be used.  A normal loop may have multiple single section blocks and the last may be empty.  The loop will be performed for as long as one of the conditionblocks is true.  Code following the first conditionblock that evaluates to true will be executed untill the first non fall through conditionblock or the end of the loop.  If the last conditionblock is empty, the loop will repeat itself infinitly.  The code following the empty condition is executed when no other conditionblock evaluates to true.
  • If the first statement is a conditionblock and it contains 2 sections, a space loop will be performed.  The first section should contain a calculation that results in a space.  The second section needs to be a space entity variable of the type of the space.  In this construction, only 1 conditionblock is allowed.  The loop will be executed for each entity in the space
  • If the first statement is a conditionblock that contains 3 sections, a for loop is used.  There can only be 1 conditionblock in this situation.  The first parameter initialises the for loop, the second parameter determins when to quit the loop, the last parameter is the incrementor.  This is the same schema as with C for instance.
  • If the last section is a conditionblock, a repeat loop is used.  This construction can only have 1 conditionblock with only 1 section.  Code inside the loop will be executed for as long as the conditionblock evaluates to true.
examples:

//a case loop: perform for as long as aVal is 'something', 'test' or 'something else'.  
//If it is test, perform the code: doy doz aval= 'something'
aVal: string
aVal = 'test'
{aVal
<-'something'->
dox
aVal = ''
<-'test'-> doy
>-'something else'->
doz
aVal = 'something'
}
//a mormal loop with multiple blocks and the last block as empty: do eternal loop
{<-aVal == 5 && aVal2 > 2-> dox
<-aVal== 5-> doy
>-aVal==6->
doz
break //exits the loop
<--> doz
}
//a mormal loop with interface calls as conditionblocks: perform for as long
//as one of the values in the conditionblock can be retrieved from the interface
{<-aval-> dox
<-1-> doy
<-3-> doz
}
//a repeat loop: do for as long as aval > 0
{ dox
doy
aVal -= 1
<-aVal > 0->
}
//a space loop: loop through all the characters of a string
aStr: string
aEl: aStr.Entity
{<-aStr\ iEl-> dow(iEl.Entity)} //iEl.Entity returns the content of the space entity,
//in this case the char a for loop with increment 1 and a declare in the conditionblock:
//loop for as long as i <> 10
{<-1; i:int; 10-> dow(i)}
//a for loop with our own increment
{<-i: int =0\i < 10\i+=1->dow(i)}

break and continue


"break" 
"continue"

Use the break statement within loops to pass control to the first statement following the innermost loop block.

Use the continue statement within loops to pass control to the end of the innermost enclosing end brace belonging to the looping construct; at which point the loop conditions are re-evaluated.

inherited

"inherited" ["(" [Condition {"," condition} ] ")"]

The "inherited" statement can be used in expressions that have been defined with the override keyword. It will call the expression of the parent.

If the expression is the loop implementor of a store, the inherited keyword is overriden.  In this case, it represents the position where the inner loop code should be performed and should return 1 value: a pointer to the current record.

Constructor call


[designator [ "(" [calculation {"," calculation}] ["\" [assignment {"," assignment} ] ] ")" ]   
| typedecl
]

A constructor is used to create objects of structured types such as structs, spaces and stores.  When used with a designator, it will create a dynamic object (on the heap) and fill in the supplied default values.  When used in a type decleration, it will define a different function to call than the default constructor + give it initial values different than the default ones as defined in the object type.  In a type decleration, the constructor can be used on static variables or on owned pointers, in which case the object will be created dynamically when the owning object is created.

After supplying the values for the parameters of the constructor, you can write assigments that will override the default values for the fields of the object.  These values will be set before the actual expression is called just as the other default values of the object.

When constructing a variable, you can either call the constructor directly on the varialbe, or you can use an assign of a constructor applied on a type name.  This allows for dynamic creation of object that are of a different type than the actual decleration of the variable.

If there are no constructors supplied in the type definition, the object can not be created dynamically.

examples:

//a space definition:
space x =
var a: int = 0
constructor create =;

CreateWithPar(aval: int)
destructor destroy = ;
;
space y(x) =
var b: int = 1
//constructor is inherited from x
;

exp z.dosomething =
ax: x^
//a simple constructor
ax.create
//a constructor with initial values
ax.create(\ a = 2)
ax.destroy
//a constructor with parameters
ax.CreatewithPar(1)
ax.destroy
//a constructor with parameters and initial values
ax.CreateWithPar(1\ a = 2)
//constructing an object with type that of a parent of the actual object being created
ax = y.create
ax.destroy
//and with initial values
ax = y.create(\ b = 2)
ax.destroy
;

Destructor call

Designator

The destructor call is used to delete dynamically allocated variables of structured types.  This includes structures, spaces and stores.  When a variable ends with the name of one of the desctructors defined in the object type, the object will be destroyed.  This should only be done when the object is already created.  For undefined objects, the result will be an exception since memory for the object can not be deallocated.  After a call to the destructor, the object is set to nil.

If there are no destructors defined in the type definition, the object can not be destroyed dynamically.

examples:

aStr: string^
aStr.Create //create the string
aStr.Delete //destroy the string

Raising exceptions

"raise "(" Statement ")"

Use the raise statement to create an exception.  When an exception is created, execution falls back to the first try-except or try-finally pair above the current code (on the stack).  The parameter detirmins what needs to be raised.  This is usually a constructor of an exception type.

When there are no parameters supplied, it will reraise the current exception.  This can only be used inside a try-except block.

examples:

raise(Exception.Create('internal error'))

Exception Handling

Gene handles exceptions by the usage of try - except/finally blocks.  If an exception occurs within a try block, self created by the raise statement or generated by the hardware, execution will continue at the first statement within the except/finally block.

Try statements may be nested.  When an expression generates an exception that is not handled within that expression, the stack will be unwinded to the first try block that called the expression.

For more information on the various exception blocks, see:

Except statements


"try"
{statement}
"except" [memloc]
statement
{"except" [memloc]
statement
}

The Try-Except statement block is used to handle errors that occured in the try block.  There may be multiple except parts, each with a unique variable type, and the last may have no variable.  The type of the variable is the name of the exception type that the block should handle.  The except block with the first type decleration that the thrown exception inherits from is executed.

The empty except part is executed if no other block matches the exception that should be handled.  This allows you to handle all exceptions, no matter what the type is.

This is the same mechanisme of exception handling found in Delphi or C++.

You can reraise the exception within an except block by using the 'raise' statement witout parameters.

Variables declared inside the try or except blocks are local to that block.  In the case of owned pointers, the associated memory will be freed and, if it are objects or complex types, the default destructor will be called.  Statically declared objects of complex types will also be freed and their default destructor will be called.

An exception block can only have 1 statement inside it.  If you need multiple statements, you can write them inside an option or loop statement with or without conditions. 

Note: if you use a loop statement without condition block, you have an ethernal loop, the only way to stop it is by writing a break or return inside the loop.


examples:

try
a: float = 5.0
a /= 0 //oeps, division by zero
except aErr: EDivByZero
screen.WriteChar('oeps, devision by 0')
except
screen.WriteChar('unexpected error')

Finally statements


"try"
{statement}
"finally"
statement

The try-finally statement block is used to make certain that the code in the finally part is executed if there is an exception or not.  This is often used to do clean up of dynamic memory.  Try-finally statements may be nested and used together with try-except statements.

Variables declared inside the try or finally block are local to that block.  In the case of owned pointers, the associated memory will be freed and, if it are objects or complex types, the default destructor will be called.  Statically declared objects of complex types will also be freed and their default destructor will be called.

examples:

a: string^
a.Create
try
b: float = 5.0
b /= 0 //oeps
finally
a.destroy //make certain that the string is always freed.
;

Designators


[identifier | SpaceSelector | "Self" ] 
{ "." [identifier | SpaceSelector | SpaceEntitySelector]
|"(" [Condition {"," Condition}] ")" ["@" Factor]
|"@" Factor
|"^"
}
SpaceSelector = ["first" | "last | "NrRecords" | "store" ]
SpaceEntitySelector = ["Next" | "Prev" | "Pos" | "entity" ]

A designator is used to define an access path to a variable, field, type or expression.
A designator can begin with one of the following items:
The path can be extended by the 'dot' operator followed by:

If the previous item in the path is the name of an expression, you can provide it with parameters and a possible interface that it needs to use.  Check expression calls for more info.

If the previous item in the path is a variable of a space type, you can provide it a filter. Check Space filters for more info.

Use the '^' operator to deref pointers.  This only needs to be used at the end of the path to indicate that you don't want the ref.  The operator will be inserted automatically in the middle of the path as necessary.

Use the '@' operator if you want to access an element in an array (if the previous item was a pointer or array) or if you want to get to a specific entity in an interface.  The factor gives the integer value of the index.  In case of arrays, this is always 0 based.  If it is on a space, it depends on the implementation of the store.

examples:

screen //select the screen object which is a global var
aStr: string
astrEnt: string.entity
astrEnt = aStr@1 //select the char at the second pos in the string (string is 0 based)
aPChar: char^
aPChar = aStr.Store.Data //select the internal data of the string through the store
aChar: char
aChar = aPChar@1 //select the second char in the string pointer (pointer array is 0 based)
aChar = aPChar^ //select the first char through a pointer deref

Casts

TypeDecleration "("  Condition ")"
Casts transform a variable from one type to another.  The following casts are legal:

Space filters

"(" [Condition] ")"

A space filter is used to access the entities of a space in a designator.  Depending on the location of the designator (lValue or rValue) and the operator used, a loop will be performed or the first element meeting the criterea will be returned.  So this can be used as a shorthand for space loops.  The space filter can be appended with another designator path.  This will be used for each entity returned by the space filter.  Space fitlters can therefore be used in sequence.  The same action will be taken for filter (loop or return first element).

The condition should return a boolean value, indicating if an entity meets the filter or not.  Fields of the space entity associated with the space can be accessed directly.  To access the entity itself in the condition, use the 'entity' keyword.  This is usefull if the space entity is a base type (such as an integer) or pointer.

The following list gives an overview of the actions taken in the different situations:


Assignment - bitshifting
Copy - move
Calculation: The first entity is returned, the calculation will be performed with this item only.
Interface statemtent: All entities of the space that meet the criteria will be handled by the interface.
Space loop condition: The loop will only return entities that meet the criteria.

examples:

aStr: string
aStr = 'aabbaabccaa'
//cange all 'a' to d
aStr(entity == "a") = "d"
//count all the 'b'
aChar: aStr.Entity
Counter: int = 0
{<-aStr(entity == "b")-> Counter += 1}
//set all the people's house number who are male to 1, this supposes
//we have a space 'peoply' with as subrecord, address
People(Gender == "m").Address.Nr = 1
//delete all the people who are not male or female
People(Gender <> "m" && Gender <> "f") ->> nil
//print all the people address (send to interface)
People().Address
//get the first female
aTemp: People.Entity
aTemp = People(Gender == "f")
//add the house nr of the first female to the counter
counter += People(Gender == "f").Address.Nr

Operators

Operators are tokens that trigger some computation when applied to variables and other objects in an expression.

The Groups of operators are:

The following precedence rules are apply to the operators.

Structured types can overload all operators except:
  • @ (array index, space index, interface-expression link, get address)
  •  ! (not operator)
  •  ` (complement operator)
  •  sizeof  (sizeof operator)
  • is  (is operator)

Arithmetic Operators


["+"|"-"] term
term ["+"|"-"] term
factor ["*"|"/"|"%"] factor

Use the arithmetic operators to perform mathematical computations.
The unary expressions of + and - assign a positive or negative value to a term.
+ (addition), - (subtraction), * (multiplication), and / (division) perform their basic algebraic arithmetic on all data types, integer and floating point.
% (modulus operator) returns the remainder of integer division and cannot be used with floating points.

Assignment Operators

Condition ["="|"+="|"-="|"*="|"/="|"%="|"<<"|">>"|"<<-"|"->>"] Condition

operator meaning:

The = operator is the only simple assignment operator, the others are compound assignment, space or bitshift or operators.
In the expression E1 = E2, E1 must be a modifiable lvalue. The assignment expression itself is not an lvalue.

The expression
E1 op= E2
has the same effect as
E1 = E1 op E2
except the lvalue E1 is evaluated only once. For example, E1 += E2 is the same as E1 = E1 + E2.

For both simple and compound assignment, the operands E1 and E2 must obey one of the following rules:

Note:    Space characters separating compound operators (+<space>=) will generate errors.

The expression
E1 ["<<"| ">>"|"->>"|"<<-"] E2
must obey the following rules

The "<<" and ">>" operators are context sensitive and can also be used as bitwise opertors.

Bitwise operators

term ["|"|"~"] term
factor "&" factor
`factor
condition ["shl"|"shr"] condition

Use the bitwise operators to modify the individual bits rather than the number.  the operators will perform the following actions
Both operands in a bitwise expression must be of an integral type.
Note: "<<" and ">>" are context sensitive and can also be used as assignemnt operators

Logical Operators

!Factor
OrCondition "||" OrCondition
BoolCalculation "&&" BoolCalculation

Operands in a logical expression must be of scalar type.

@ Operator

designator "@" factor

The 'At' operator is used to:

Reference - derefence Operators


"@" Factor
designator "^"

The @ and ^ operators work together to reference and dereference pointers.

Use the reference operator (@) to get the address of a memory location pointed to by a designator.  Note: taking a reference of a property is only allowed if the property reader part is a variable.  If it is an expression, Gene can't take a reference and will produce an error.
Use the dereference operator (^) in a designataor to get a pointer's value.

Note:    @ can also be the at operator.

Relational Operators

Calculation ["=="|"<"|"<="|">"|">="|"<>"] Calculation

Use relational operators to test equality or inequality of expressions. If the statement evaluates to be true it returns a nonzero character; otherwise it returns false (0).

==  equal
>   greater than
<   less than
>=  greater than or equal
<=  less than or equal
<>  Different

In the expression
E1 <operator> E2
the operands must follow one of these conditions:
Note: the "==" operator can also be used as a type operator.

Type Operators

"sizeof" "(" Designator ")"
condition "==" condition
condition "is" condition

The sizeof operator is used to inspect the size of a designator.  This can be either a variable or a type name.   The result is an integer indicating the size in bytes of the object or type.  If the Designator is a static array, it will return the size * the number of elements.  If the designator is a pointer, it will return the size of the pointer.  To get the size of the value the object is pointing to, deref the designator.  If the designator points to a complex type without ref (space or store), the result is the size of the type that the variable was declared with, not the actual size of the object (it could be a descendent of that whith what it was declared).

The "==" and "is" operators are used to compare the types of an object (variable) of a complex type. 

In the expression
E1 ["=="|"is"] E2
Either E1 or E2 needs to be a complex type and the other one a type name or variable of type idtype.
The "==" operator will see if the variable is of the exact type as requested.  The "is" operator will check if the variable is of the requested type or one of it's parents.

examples:

space x =
var a: int
constructor create = ; default
destructor destroy = ; default
;
space y(x) =
var b: int
;

a: x^
a = y.create
[<-a is x-> //this will return true
<-a is y-> //this will return true
<-a == x-> //this will return false
<-a == y-> //this will return true
]

Precedence rules

There are 8 precedence categories, some of which contain only one operator. Operators in the same category have equal precedence with each other.  Each category has an associativity rule: left to right, or right to left. In the absence of parentheses, these rules resolve the grouping of expressions with operators of equal precedence.

The precedence of each operator in the following table is indicated by its order in the table. The first category (on the first line) has the highest precedence. Operators on the same line have equal precedence.

Operators
Associativity
@ (array - space index, interface)
left to right
! ` @ (get address) sizeof
right to left
* / % &
left to right
+ - | ~
left to right
shl shr
left to right
== < <= >= <> is
left to right
&&
left to right
||
right to left
= += -= *= /= %= << <<-
right to left
>> ->>
left to right

Assembler blocks


"asm"  "[" {AssemblerStatement} "]"
AssemblerStatement = Opcode {operands}

Use assembler blocks to specify code snippets written in assembler.  

The operands may be gene specific designators.  In this case, they will be replaced with their assembler equivalents

examples:

asm[
move EAX @Screen //move a ref to the global screen object in the eax register
]

Escape sequences

The pipe symbol ("|") is used to introduce an escape sequence which allows the visual representation of certain nongraphic characters. For example, the constant |n is used to the single newline character.

This can be used in constant string and char declerations to insert certain symbols that can't be depicted otherwise

Currently, the following escape sequences are supported:

Escape sequence
meaning
a
audible bell
b
backspace
f
formfeed
n
newline
r
carriage return
t
horizontal tab
v
vertical tab
'
single quote
"
double quote
|
vertical line

examples:

'This is a test |nthat needs to be put on a new line'
"|"" //a double quote char