You are here: start » geeny » home » intro

Introduction

The core of geeny is its database. The database is object oriented, meaning that every record in the database may be an object with its own properties and responses. Record oriented databases, in comparison, contain only records with fixed properties, and no responses.

A geeny database exists in two stages. The initial stage is a template database, which only contains templates for possible object instances, and the script that defines responses for such objects. This stage of the database is its source form, the stage designed by the programmer. Physically, the template database exists as any number of source files (.gee) inside the source folder tree, or as a single precompiled file (.ged).

When the geeny machine starts up, any number of template databases, or zones, is loaded. The second, runtime stage of the database exists in memory, and contains any number of instances based on any template from any zone. It is either loaded from a memory snapshot file (.ges) or booted up from the scratch using an initialization script from the ‘MAIN’ zone.

After the machine has booted up, it loops through a maintenance sequence, the duration of which determines the frame rate of screen update. The sequence follows system events and reads input from the user, channeling it to the scripted objects and allowing them to update their state, render graphics or sound, or manipulate other engine and media resources. The sequence executes in a loop until program is terminated.

Database

A database object, while in its template stage, contains the following data:

BEGIN            object        MY_CLASS  
  OBJECT         Parent      = MY_BASE_CLASS 
  OBJECT         Scripta     = @ my_class 
  OBJECT         Datatype    = import
  INTEGER        Prop1       = 10    
  FLOATS         Prop2       = { 5.5 , 10 , 14.6 }       
  REFERENCE      Game        = GAME
END

Object type: object, player, game, etc. Object type declares the template’s function within the maintenance sequence, and specific behavior related to that sequence. The majority of all objects will simply be of type ‘object’.

A list of properties. A property is defined by: a unique, single atom name (ie, ‘Prop’, ‘Single_Prop’, ‘Prop12’), its index within the list (first property has index 1, second has index 2, etc), its type: single token (INTEGER, ATOM, REFERENCE...) or list (INTEGERS, ATOMS, ARRAY), and its value. The type may also be nul, in which case it is not defined, and also evaluates to nul.

Datatype declares object flags for system related tasks, such as save, load and garbage collection. Scripta lists all the script source files to be attached, hierarchically, to this object.

Parenthood: An object template inherits all properties from its parent, its datatype and, when specified, its script list. Only if the inheritance operator @ appears in the Scripta, the parent’s script list will be included, by insertion and at the same position in the list. When Scripta is not specified, however, the inheritance is automatic and the parent’s script list is used for the object. To make an inherited object without any script whatsoever, assign an empty field to the Scripta property (OBJECT Scripta = ).

When an object is instantiated in runtime, its properties and their default values are copied from the template. The files in the script list are parsed for branches and evals, which are then linked to object instance. The instance itself is also assigned a replica ID#, which is unique for each instance within the group of all instances of the same template. If the instance is declared as static, its assigned ID will be #0. Static objects may easily be accessed using only their template name.

Branches and evals represent the scripted responses of the object. Within the context of other object oriented languages, they may be considered member methods or functions. The scope of a branch is its object: a branch may directly access its properties and call other branches and/or evals. An eval is a faster and simpler version of branch that lacks local scope and variables. Also, evals may be generated, compiled and linked to objects in runtime.

A branch may return a value. This must be done explicitely, using the return keyword. Evals always return a value, which is the result of the last expression of the eval. Generally, an eval behaves like a list of expressions separated by operator ;.

eval efoo(param1, param2)  
  param1 + param2
end
 
eval efoo2(param1, param2) efoo(param1 * param2, 4)
 
eval efoo3(param1, param2) param1 / param2
 
branch bfoo(param1, param2)
  INTEGER i = efoo(param1, param2) + efoo2(param1, param2)
  return i + efoo3(param1, param2)
end
 
eval efoo4(param1, param2) param1 ; param2 //returns param2
 
eval efoo5(param1, param2)
  param1
  param2 //returns param2
end

The Scripta script list is parsed from left to right. When another version of the same branch is found again later in the list, the new branch overrides the previous one. It is still possible to call the overriden branch from the new one, using the inheritance operator @. Using the inheritance operator anywhere else will produce undefined results.

This rule only applies to branches, not evals, which are simply replaced with newer versions.

branch overloaded(param)
  return @.overloaded(param)
end

Scripting

Scripting in geeny is similar to programming in other object oriented languages. Inside a branch, statements are executed sequentially. Flow control statements like if and while may be used to alter the order of execution. Object properties and local variables allocated on execution stack may be accessed and used. Using calls on references to database objects and engine object instances, execution may be redirected into another branch, eval, or system function.

The scope of a branch are its parameters, local variables, and object properties. Global properties may also be defined, in that case, they are available to any branch in the zone, or, using their full names, from any other zone.

While global properties, local variables and parameters are always addressed directly from the script, indirection may be used to address object properties:

BEGIN       object    OBJ_EXAMPLE
  OBJECT    Scripta   = Example
  INTEGER   prop1     = 10       //object property
  INTEGER   prop2     = 11       //object property
END
 
BEGIN       object    OBJ_EXAMPLE2
  INTEGER   prop2     = 12
END
 
BEGIN       global    ALIAS
  REFERENCE GlobalRef = OBJ_EXAMPLE2 //reference to static instance OBJ_EXAMPLE2#0
END
 
branch      example(  INTEGER param)
  INTEGER   local1    = prop1    //addressing a local property directly
  INTEGER   local2    = $.prop2  //direct addressing using self operator $
  ATOM      prop_name = 'prop2'
  INTEGER   local3    = $.(prop_name) //indirect addressing
  prop1               = param
  $.(prop_name)       = param
  INTEGER   local4    = GlobalRef.(pname) //12
end

Indirection may also be used when calling another branch or eval:

eval        plus(x, y)  x + y
 
eval        minus(x, y) x - y
 
branch      average(x, y)
  return    (x + y) / 2
end
 
branch      example(INTEGER x, INTEGER y)
  FLOAT     localx = 10.0
  FLOAT     localy = 20.0
  FLOAT     floatres = plus(localx, localy) //direct local call
  INTEGER   intres = $.minus(x, y)          //direct call
  ATOM      foo = 'minus'
  intres        = $.(foo)(x, y)             //indirect call
  foo           = 'average'
  floatres      = $.(foo)(localx, localy)   //indirect call
end

Using the same syntax, system functions such as rendering, sound, database management and similar, may be called, or methods on engine objects invoked. There is no distinction between calling a system function or a local branch, and system function take precedence, therefore:

branch rendSet() //legal, however system function with that name exists
  ..
end
 
branch rend_Set()
  ..
end
 
branch example()
  rend_Set()   //local call
  rendSet()    //system function
  $.rendSet()  //local call using self operator $
  INSTANCE texture = rendGetTexture('some_texture')
  INTEGER width = texture.getX() //engine object call
end

Types and operators

The basic types of geeny are INTEGER, FLOAT, VECTOR and ATOM. While the former three are numeric, representing an integral, a floating or fixed point, and a 4-dimensional vector of type FLOAT, the latter is symbolic. Atoms are used as names for types, classes, templates, scripts, branches, evals.

There is a list of mathematical operators to be used with numeric types. In general, operators support mixed types, ie:

INTEGER a = 5
FLOAT   b = 10.0
VECTOR  c = |1 2 3|  //equivalent to |1.0,2.0,3.0,0|
b *= a               //or b = b * a -> b == 50.0
c *= b               //or c = c * a -> c == |50.0 100.0 150.0 0|

As a general rule, the first operand determines the type of result:

VECTOR  a = 2        //equivalent to |2 2 2 2|
FLOAT   b = 10.0
lwriteln(c * b)      //|20.0 20.0 20.0 20.0|
lwriteln(b * c)      //20.0

To address a vector component, either the component operator | or the array operator [] may be used:

VECTOR  a = 2        //equivalent to |2 2 2 2|
a|x       = 0        //equivalent to a|r = 0 or a[1] = 0
a|y       = 0        //equivalent to a|g = 0 or a[2] = 0
a|z       = 0        //equivalent to a|b = 0 or a[3] = 0
a|w       = 0        //equivalent to a|a = 0 or a[4] = 0

Property types that may hold reference to another object are REFERENCE and INSTANCE. While a reference points to a database object, an instance points to an engine object. Using the member operator . on a reference, its properties may be accessed, or its branches and evals may be called:

INTEGER i = 50         //declaring and assigning a local variable
REFERENCE obj = 'TEST' //selects TEST#0. The object is created if it does not exist
INTEGER obj.prop1      //prop1 is created on object if it does not exist
obj.prop1 = i          //a property is written into
i = obj.prop1          //reading object property
obj.prop1 = 1          //accessing object property
 
obj = new 'TEST'       //create new instance of TEST with unique, non-zero replica ID
obj.foo()              //call object's branch or eval

A geeny programmer has full control over all database objects, their properties and responses. This is not the case with engine objects. These objects are provided by the engine, their properties are hidden away from the programmer, and their responses are predefined, hard-coded interfaces available to the programmer for manipulating system and media resources like sounds, textures, sprites, 4×4 matrices etc.

rendLoadTextureLibrary('System')
INSTANCE tex = rendGetTexture('Font')
tex.rendText(10, 10, 'Hello')  //using object interface...
 
rendSetTexture(tex)            //or alternatively, using a system function:
rendText(20, 20, 'Hello')
rendSetTexture()
 
INSTANCE sound = soundGetSample('Beep')
soundPlay(sound)
 
INSTANCE matrix = new:matrix()
matrix.setIdentity()
free(matrix)

While the database engine does all the persistence related maintenace tasks on database objects, such as garbage collection or saving the state to a file, the programmer alone is responsible for such maintenance of engine objects. The only exception to this rule is the reboot sequence, which automatically destroys every engine object in memory.

Zones

When multiple database sources are loaded, each one is assigned with a name to represent a single zone. Database objects may freely access and create objects directly from their own zone. To address an object from a different zone, object’s full name has to be used:

BEGIN       object         TEST
  OBJECT    Parent         = PARENT
  OBJECT    Scripta        = @ test
  REFERENCE obj            = OBJ
  REFERENCE obj_from_zone2 = ZONE2:OBJ
END
 
BEGIN       object         TEST2
  OBJECT    Parent         = ZONE2:PARENT
  OBJECT    Scripta        = @ ZONE2:test
END

Database sources are installed in a specified order. The order of installation does not influence accessibility of templates across zones. It does, however, influence the accessibility of global properties. Specifically, a global property may only be adressed from any zone installed after the zone that the property belongs to. For this reason, the ‘MAIN’ zone is frequently installed as the last one, making it capable of adressing all the global properties in the database.

As an alternative to using hard-coded full names in template definitions, the script may also generate full names on the fly using the fullname system function, or by temporary changing the zone using the zone system function.

REFERENCE global         = GlobalReference       //global property
global                   = ZONE2:GlobalReference //global property from ZONE2
REFERENCE obj1           = new 'OBJ'
REFERENCE obj_from_zone2 = new 'ZONE2:OBJ'
 
//alternatively
 
REFERENCE obj_from_zone2 = new fullname('ZONE2', 'OBJ')
 
//or
 
ATOM      oldzone        = zone('ZONE2')
REFERENCE obj_from_zone2 = new 'OBJ'
zone(     oldzone)

When a branch or eval is executed, its object’s zone is always automatically selected. Therefore, using the zone function will not alter the behavior of any branch or eval being called after the zone was changed. Also, the zone only remains changed within the scope of the branch, and is automatically restored on branch exit.

Keywords and identifiers

Keywords in geeny are always recognised within a context, therefore, most of them may be used as identifiers for local variables, object and global properties, branches and evals.

BEGIN     object    TEST
  ATOM    name      = 'name'
  ATOM    object    = 
END
 
branch    name(param)
  if name
    return name
  else
    return $|name   //keyword
  endif
end
 
branch    test()
  lwriteln(name)    //'name'
  lwriteln($|name)  //'TEST'
  lwriteln(name())  //'name'
  name = nul
  lwriteln(name())  //'TEST'
end

For certain keywords, extra care needs to be taken when they are used in such way: for, next, if, else, elseif, endif, branch, eval, end, while, endwhile, break, continue, switch, all operators and system functions. These keywords may not be used as local variable names, and will only be interpreted in correct way when they are associated with a member operator:

BEGIN     object    TEST
  INTEGER if        = 1
  INTEGER +         = 2
END
 
branch    *(param)
  ...
end
 
branch    name(param)
  lwriteln(if)      //incorrect!
  lwriteln($.if)    //1
  lwriteln($.+)     //2
 
  *()               //incorrect!
  $.*()             //ok
end

All identifiers are atoms. Atoms containing a single zone operator : are path atoms, and normally used to generate script and database object full names, ie ‘zone:script’.

Path atoms may also be used to name branches, evals, system functions and engine object interface functions. This syntax is allowed so that functions may be organised in a transparent way, and has no other consequences or side effects.

branch motion:idle(speed)
  ..
end
 
branch motion:run(speed)
  ..
end
 
branch motion:walk(speed)
  ..
end
 
branch act:move(type, speed)
  $.(fullname('motion', type))(speed)
end
 
branch act()
  act:move('idle', 10)
end

Lists and arrays

Every basic type in geeny has its list variant: INTEGERS, FLOATS, VECTORS, ATOMS, REFERENCES and INSTANCES. In addition, type ARRAY holds a list of pointers to properties of any type.

To address an item in a list, array operator [] may be used. The operator accepts a single INTEGER index, in which case the result is a reference to single item in the list. Alternativelly, the operator also accepts a list of INTEGERS, in which case the result is a new list containing all indexed items from the original list. The type of the resulting list is identical to the type of original list.

Indexes in geeny are 1-based. Reading an element that is not in the list (ie index 0, negative index or index that lies beyond the list end) returns nul, while any attempts to write outside the list boundaries are ignored.

INTEGERS list = { 10 20 30 40 }
INTEGER  i    = list[1]     //10
i             = list[0]     //nul, converts to 0
i             = list[9]     //nul, converts to 0
list[4]       = 5           //{10 20 30 5}
list[0]       = 5           //ignored
 
INTEGERS l2   = list[{1 2}] //contains {10 20}
l2            = list[l2]    //contains {}
 
list[{2 3}]   = 15          //no effect, identical to:
INTEGERS l3   = list[{2 3}] //however in the above case instead of l3
l3            = 15          //becoming {15} the hidden result is lost
 
//but:
list[{2 3}]  := 15  
//or:
[list[{2 3}]] = 15          //list now: { 10 15 15 5 }
[list[{2 3}]] = {1 2}       //list now: { 10  1  2 5 }

The majority of mathematical operators and functions in geeny take arrays as operands or parameters. When the case is such, the operation is simply executed on every element in the list. The result, if present, is a list of the same type. When two or more lists are processed in parallel, the lists with insufficient number of elements are cycled through.

INTEGERS l1   = {10 20 30 40}
INTEGERS l2   = {1 2 3 4}
l1           += l2          //l1 now: {11 22 33 44}
l1           += {100 200}   //l1 now: {111 222 133 244}
//and:
l1            = l1 + 1      //l1 now: {112 223 134 245}
//however:
INTEGER  i    = 1 + l1      //i == 113, first item from the list used

Because all type checking is done at run time, evals and branches will behave differently when called with parameters of different type. In order for this to work, however, parameters must be passed by reference:

eval add(x, y)  x += y
 
branch test
  INTEGER       x = 10
  lwriteln(     add(x, 5))     //x = 15, output 15
  FLOAT         y = 3.0
  lwriteln(     add(y, 1))     //y = 4.0, output 4.0
  FLOATS        l = {1 2 3 4}
  lwriteln(     add(l, {1 2})) //l and output {2.0 4.0 4.0 6.0}
end

ARRAY is an additional list type that may contain pointers to properties of any type. This is an important distinction from other lists that all contain items themselves. The conversion between lists and array is automatic, but to store pointers to specific items into an array, the enclosing array operator may be used:

INTEGERS list = {10 20 30 40}
ARRAY    arr  = list
arr[1]        = 50
lwriteln(list)              //{50 20 30 40}
arr           = [list[{2 3}]]
arr[1]        = 60
lwriteln(list)              //{50 60 30 40}
 
//while
arr           = list[{2 3}] //arr now points to temporary list on stack
arr[2]        = 70
lwriteln(arr)               //{60 70}
lwriteln(list)              //{50 60 30 40} !!

From the above example, it becomes obvious that:

[list[{2 3}]  = 15 
//or
list[{2 3}]  := 15

is equivalent to:

ARRAY temp    = [list[{2 3}]]
temp         := 15
[temp]        = 15
//or
temp[1..4]   := 15
[temp[1..4]]  = 15

There is a danger of returning an ARRAY as result, when its items point to either local variables, or temporary properties on the stack, that are already out of scope, ie:

INTEGERS list = {10 20 30}
ARRAY result  = list
return result                 //unsafe!
 
INTEGERS $.list = {10 20 30}  //static property on the object
ARRAY result    = list
return result                 //ok

Arrays may be declared as object properties. When array property is declared in object template, the initial value is shared by all objects generated from that template. Additionally, altering the contents of these shared arrays will be permanent and will not be reset upon database reboot.

BEGIN object TEST
  ARRAY Test = atom 10 15.0
END
 
branch test
  CLONES test = new repeat('TEST', 2)
  lwriteln([test.Test])  // {atom 10 15.0} {atom 10 15.0}
  test[1].Test[3] = 3   
  lwriteln([test.Test])  // {atom 10  3.0} {atom 10  3.0}
  test[2].Test = 3
  lwriteln([test.Test])  // {atom 10  3.0} {3}
end

In addition to mathematical operators and functions, some database oriented operators and functions may accept lists as operands as well. These operators include the member operator . and allocation operator new:

REFERENCES test    = new {'TEST_1' 'TEST_2' 'TEST_3'} 
 
INTEGER    test.prop1    //INTEGER prop1 created on all objects in the list
FLOATS     test.prop2    //FLOATS prop2 created on all objects in the list
 
ARRAY  p   = test.prop1  //p contains TEST_1.prop1, TEST_2.prop1, TEST_3.prop1
[p]        = 15          //set all three props to 15
INTEGERS r = p           //{15 15 15}
 
INTEGER  x = test.foo()  //call foo on every object in test, 
                         //last result is returned
INTEGERS y = [test.foo()]//call foo on every object in test, 
                         //all results returned as ARRAY

In a similar fashion, INSTANCES may be use to call the same function, when it exists, on any number of engine objects. Like with database objects, calling a function that does not exist is ignored and the result is nul.

Indirection may be used in connection with property adressing or calling. When indirection is achieved using single ATOM variable, a single property is addressed, or a single branch, eval or engine object function is called. When indirection is done with a list of ATOMS, every named property is addressed, and every named function is called:

REFERENCE obj   = 'OBJ'
ARRAY props     = obj.({'prop_1' 'prop_2' 'prop_3'})
ARRAY results   = [obj.({'foo1' 'foo2' 'foo3'})()]
 
INSTANCE tex    = rendGetTexture('test')
ATOMS seq       = {'getX' 'getY' 'getXFrame' 'getYFrame'}
INTEGERS dims   = [tex.(seq)(:)]
 
REFERENCES objs = {'OBJ1' 'OBJ2' 'OBJ3' }
props           = objs.({'prop_1' 'prop_2'})

When listed indirection is used along with REFERENCES or INSTANCES, as in the case above, the objects are looped through, then for each object, the indirection list is iterated. The result would be: {OBJ1.prop_1, OBJ1.prop_2, OBJ2.prop_1, OBJ2.prop_2, OBJ3.prop_1, OBJ3.prop_2}. Same rule applies to calling.

Indirection and listed indirection may be used on INSTANCE and INSTANCES as well.

Strings

Although type STRING exists, it is merely an alias for INTEGERS type. A string, in geeny, is a list of integers, each of them representing a single character. This allows for wide character strings (not yet supported), also, additional formatting information may be stored in the higher bits.

Geeny does not support literal strings within the script code. Instead, strings may be loaded from the string portion of the database zone. To load a string from another zone, select it using the zone() system function:

STRING str = string:load('STRING')
zone('ZONE2')
STRING str_from_zone2 = string:load('STRING')

System and engine object interface functions accept either ATOM or STRING/INTEGERS as string parameter. When ATOM is used, the string is automatically loaded from the database:

rendText(x, y, 'STRING')
//equivalent to:
STRING str = string:load('STRING')
rendText(x, y, str)

When the string being loaded does not exist in the database, the engine generates a string that simply contains the missing name.

A source file for the strings database has the following syntax:

#STRING_1
text ... text ... text ...
text ... text ...
...
#
#STRING_2
...
#
ignored text or whitespace
##UNFORMATTED_STRING_1
...
#

The parser is white-space sensitive, # at the start of the line has to be followed by the name of the string immediately. In a multiline string, all newline and leading/trailing space information is removed when compiled, and space character is inserted between the lines. When double # is used, only newline characters are removed and the lines are simply concatenated.

Whitespace or any other text between string definitions is ignored by the compiler.

Folder structure

A single database source is compiled from a single source folder and a matching configuration file. Assuming the database resides in folder Projects, its name is Database, the folder structure is:

Projects/Database.gei

Configuration file, if present. The database may not be compiled without the configuration file, as it contains the necessary compilation settings.

Projects/Database.ged

Precompiled database, if present. If not, the database will compile on start up. The precompiled database incorporates default settings as specified by Database.gei at compile time. If a configuration is present when running a precompiled database, settings in the .gei file override the settings in the .ged file.

Projects/Database/

Database source folder.

Projects/Database/Object/

All .gee files located in this folder are loaded and scanned for template and global property definitions (BEGIN..END blocks). If the file contains at least one eval or branch definition, it is recognised as script and given the name of the file itself (minus the extension .gee). Thus, it becomes possible to encapsulate the whole object template definition, its script and its properties, in a single file, ie:

Projects/Database/Object/Example.gee

BEGIN object Example
  OBJECT Scripta = Example 
  INTEGER Prop1 = 0
END
 
BEGIN object SupportingObject
  OBJECT Parent = Example
  INTEGER Prop1 = 0
END
 
eval inc() Prop1 += 1

Like template, string, and all other names in geeny, script names are case sensitive.

In this case, the Example.gee is first scanned for templates, finding objects Example and SupportingObject. Object Example uses the same file, Example.gee as script. So does SupportingObject, inheriting its parent’s script by default. For many situations, this is a convenient method.

The scanning process is repeated for any subfolders of Object, their subfolders etc. As long as the file only contains template definitions, duplicate file names are allowed. Script files, on the other hand, require a unique name regardless of their location in the subfolder tree.

Project/Database/String/

All string source files are collected from this folder. Subfolders are not scanned. While .txt files are compiled as described in the chapter above, .bin files are embedded in their raw form. To load a binary string, in which all characters, including 0, are allowed, binary:load() system function is called using the name of the file (minus its extension) as parameter. The identifier is case sensitive.

Project/Database/Brush/
Project/Database/Texture/
Project/Database/Sprite/
Project/Database/FX/

Contain .gee files with engine object templates. Although engine objects are not instantiated like database objects, simple object factories are provided to reduce the amount of setup code in scripting. Instead of creating raw, empty objects with new operator, factories are useful to generate engine objects with preconfigured settings.

Configuration file

Maintenance loop

Garbage collection

Garbage collection is invoked using the sys:purge() system function. The function will launch the garbage collection process in the beginning of the next maintenance loop.

The process eliminates all unreferenced objects from the database, invoking the destructor branch free(’purge’) for each and then freeing the allocated memory. In addition to that, all objects previously scheduled for deletion using the free() system function are also removed, and all their references in the database are cleared. As the free() function itself invokes the destructor, it is not invoked again when the object is actually purged.

Persistence

 
geeny/home/intro.txt · Last modified: 2015/11/18 07:37 by matija
Recent changes · Show pagesource · Login