EScript 0.7.1 (Elfriede)

EScript ist eine interpretierte, objektorientierte, C-Syntax-ähnliche Skriptsprache, die mit dem Fokus entwickelt wurde C++-Programme (relativ) einfach durch Skripting steuern und erweitern zu können. Dabei sollen insbesondere Objekte der Programmlogik direkt durch EScript Objekte dargestellt werden. Dafür ist das Klassen- und Objekt System von EScript deutlicher ausgeprägt als z.B. von LUA.


Hallo Welt-Beispiel

out("Hallo Welt!\n");
Hallo Welt!

                       SESpEES     cEStSppc
                     EEii    iE   cp     itp
                    pE iiiii irE  p iiiiii  p
                    E iiiiii E p tt ri iiiii E
                   Et iiiii pEiiSE t iS  iiii E
                   Ei iii  pE p Eiip  Etiii   Et
                   Ec i  ipE  c p p  riEcSSEEES
                    E iSEpE   icpcE  tti
                    pEp cip   SErpi tcp
                        ppi   iEit  pp
                       ipc    rEpt icp
                     iEEcittEE    iiE
                    pt       E      iE
                   pE   ii   ci E    pi
                   E    pE   S ip    S
              Etri p         E      E
               EEcctE      SE    iSEpcritrci
             cr ppi iEESSEEcicpptci   SEiiSpEpES
         ipEpciSi  ii  ii        iiirEcrri
        ii   iEptSiiiiii iEpEEEp     rEpiEEE iiiir
           tpE  pS  iiii SEEEESp irpErpEE
              cpc EpEtc     E  rStiEEt
                ic  pE ci   prpEtrpi Spt
              it   icrEEErEE   cEtE    i     pptppi
    iEcSE   ipi   Ei  p pS   E  p iEr       pc    pci
  irEi  rp tr    p  Ep   p   p ip i pp      Et iiEi rE
 Eci rii pE    cp cE  i  EEEppEr iii ipt iSEr iii  tpE
E  iiii Sc pi  r pc i  iEpt   itS  iii rEr i    irii cE
 tSiiiiii  itEE pr i tppci     itEr  ii  E   rEEpEpttE
 rS i  ictti   pc i Epi           Sp  ii iEiEpi
  ttctSEi  ppiiE i ES              tp iii EE
    i        rE   pp                ES ii ip
              p iip                iEp iii E
             pE iiEr               icp iii p
             Ei iiS                rcc iiiiE
             Er i E       Ep        Eiiii rc
             ip i tE     ipEp      Sp iii p
              Ep   EE            iEp iii pE
               ppr  tEpi        tES ii  pE
                 tpi  ipEpctcpEpp     ipi
                   EpEiiiiccrtr  itppEp
               iptc  iirttcSE  pEcciii rSSS
              rE E ic i     iES      ii  iiEt
               tE pp EEcSSSScitEirEiS  pt  iE
                                ici pE  prrc

Inhaltsverzeichnis

Über dieses Dokument

Dieses Dokument soll einen kurzen Überblick über die Sprache EScript liefern. Es kann dabei leider keinen Anspruch auf Vollständigkeit erheben. Bei Fragen oder Anregungen meldet euch bei mir.
Claudius

 // Codebeispiel
 Ausgabe von Codebeispielen
 Syntax Grammatik
Wichtiger Hinweis oder beliebte Fehlerquelle.

Interessant zu wissen; das braucht man aber vermutlich normalerweise nicht.

Datentypen

Einfache Typen (call by value)

Hinweis zur impliziten Typumwandlung:

Weitere häufig anzutreffende eingebaute Typen (call by reference)

Einige der vielen vermutlich nur indirekt verwendeten Typen (call by reference)

Sprachkonstrukte

Kommentare

Wie in C.

// Kommentar bis zum Ende der Zeile
/* Ein Blockkommentar */

Anweisungsblock {...}

Grammatik:
Block ::= '{' Statement* '}'
Statement ::= Block

Wie in den meisten C-Syntax-ähnlichen Sprachen. Zwischen { und } können Anweisungen stehen. Diese werden durch ; getrennt.

Lokale Variablendeklaration: var

Grammatik:
VarDecl ::= 'var' Identifier [...]

var deklariert eine lokale Variable innerhalb der aktuellen Funktion im umschließenden Block ( {...} ). Die Ausnahme dieser Regel ist die Deklaration von Variablen innerhalb von Schleifenbedingungen (seit Version 0.5.1). Dann gilt die Variable nicht im direkt umschließenden Block, sondern innerhalb der Schleife (also in etwa so wie in C).

// var-Beispiel
{              // in diesem Block sind a und b deklariert
    var a;     // führt zur deklaration von a im umschließenden Block
	a=1;       // einen Wert zu a zuweisen
	var b=1;   // b deklarieren und einen Wert zuweisen
	{             // in diesem Block ist ein eigenes a deklariert
		a=2;      // dem im umschließenden Block definierten a eine 2 zuweisen
		b=2;
		var a;    // führt zur deklaration von a im umschließenden Block
	}             // am Ende des Blocks verschwindet das hier deklarierte a wieder
	
	for(var a=0;a<5;++a) // das in der Schleife deklarierte a gilt nur in der Schleife
		out(a," ");
	
	out("\n a:",a," b:",b,"\n"); // a und b des äußeren Blocks ausgeben
}
0 1 2 3 4
a:1 b:2

Einige Dinge zur Beachtung:

Bedingte Anweisung: if-else

Nichts besonderes. Wenn die Anweisung in den Klammern des if auf true ausgewertet wird, wird der Block ausgeführt. Sonst wird danach oder bei einem eventuell existierenden else weitergemacht.

Grammatik:
If-Statement ::= 'if' '(' Expression ')' Statement ['else' Statement]
Statement ::= If-Statement
// if-Beispiel
if( true ){
	out("a");
	if( 1-1 == 0)
		out("b");
	else if (2-2 == 0)
		out("c");
}else out("d");
ab
Achtung! Anders als in C, wird auch die Zahl 0 als true interpretiert. Nur false und void werden als false interpretiert. Um z. B. ein Bit einer Zahl zu prüfen, braucht man daher so etwas: if( (zahl & 0x10)>0 ) ... .

Bedingte Anweisung: ? :

Wie in C...

Grammatik:
ShortIf-Expression ::= Expression '?' Expression ':' Expression
Expression ::= ShortIf-Expression
// short-if-Beispiel
out( 5<0 ? "klein\n" : "gross\n" );
out( true ? ( "a" + ((1-1==0) ? "b" : ((2-2==0) ? "c" : "")  )) : "d" );
gross
ab

Logische Operatoren: ||, &&, !

Wie in C...

Funktionsaufruf

Wenig besonderes...
Ein Funktionsaufruf wird durch eine (möglicherweise leere) Parameterliste nach einem Ausdruck, der die Funktion zurück gibt, beschrieben. Die Parameter werden durch Kommata getrennt. Funktionen können einen Wert zurückgeben.

// Funktionsaufruf-Beispiele:
out("foo"); // gib "foo" aus
var time=clock();

Funktionsdefinition: fn

Funktionen sind in EScript ebenfalls Objekte. Sie haben von sich aus keinen Namen, sondern man muss sie Variablen zuweisen, um ihnen einen Namen zu geben. Daher sieht eine typische Funktionsdeklaration, die ein Funktionsobjekt an eine lokale Variable zuweist, so aus:

var myNewFunction = fn(x){      // Funktion mit einem Parameter
	return x*x;
};
outln( myNewFunction );    // gib den Wert der Variable myNewFunction aus (und einen Zeilenumbruch).
outln( myNewFunction(7) ); // gib den Rückgabewert des Aufrufs der Funktion myNewFunction aus.
#Function
49

ACHTUNG! Beliebter Fehler: Da es sich bei den meisten Funktionsdeklarationen eigentlich um Wertzuweisungen handelt, muss diese dann auch durch ein Semikolon ; abgeschlossen werden!

ACHTUNG! Da Funktionen eigentlich gar keinen Namen haben, können sie auch nicht (wie z. B. in C++) durch eine andere Funktion gleichen Namens aber anderer Parameterliste überladen werden.

Wie in anderen Sprachen auch kann über das return-Statement ein Wert zurückgegeben werden. Der Typ des zurückgegebenen Wertes kann beliebig sein (sollte aber durch den Funktionsnamen oder den Kommentar klar werden!). Wird kein return verwendet, oder ein return ohne Parameter, dann wird void zurückgegeben.

Standardparameter

Ähnlich wie in C++ können Standardwerte für Parameter angegeben werden. Werden dann beim Funktionsaufruf weniger Werte angegeben, als die Funktion akzeptiert, werden die fehlenden durch die Standardwerte belegt.

var f = fn( a, b=1, c=2 ){ // Parameter b und c müssen nicht angegeben werden
	return a+b+c;
};
out( f(3,4) );             // Parameter c wird weggelassen und mit dem Standardwert 2 gefüllt. (3+4+2=9)
9

Variable Parameteranzahl

Um eine Funktion mit einer variablen Parameterliste zu definieren, muss der Parameter mit einem ... versehen werden. Dann werden alle Werte, die nicht den normalen Parametern zugewiesen werden, in ein Array gespeichert und dieses Array dann dem (Multi-)Parameter zugewiesen. Werden keine Parameter angegeben, ist dieses Array leer.

var f = fn( x, summands... ){ // in summands wird ein Array mit allen restlichen Parameterwerten geschrieben
	var sum=0;
	foreach(summands as var v)
		sum += v;
	return x * sum;
};
out( f(2,1,2,3) ); // 2 * (1+2+3) = 12
12

Wertebereiche für Parameter

Bei einem Funktionsaufruf kann zur Laufzeit der Typ der übergebenen Werte überprüft werden. Im einfachen Fall wird das entsprechende gewünschte Typobjekt vor den Parameternamen geschrieben. Wird die Funktion mit einem Wert anderen Typs aufgerufen, wird ein Laufzeitfehler ausgelöst.

var f = fn(Number x){ // x muss vom Typ Number sein.
	return x*2;
};
out( f(4) ); // Dies ist in Ordnung.
out( f("blabla") ); // Die Übergabe eines String wird einen Fehler hervorrufen!

Man kann aber auch komplexere Wertebereiche angeben. Dazu wird in Arrayschreibweise eine Liste möglicher Typen und möglicher Werte vor den Parameter geschrieben. Beispielsweise akzeptiert die Funktion fn( [String,3,void] x){} für den Parameter x entweder einen beliebigen String, den Wert 3 oder den Wert void.

Intern wird auf jedem Bedingungsobjekt die Memberfunktion _checkConstraint aufgerufen. Liefernt eine Bedingung true zurück, gilt der Parameterwert als valide. Der Parametertest in der Funktion fn( [Number,void] p1){} entspricht also im Wesentlichen if( !Number._checkConstraint(p1)&& !void._checkConstraint(p1) ) throw new Exception("...");

Rekursive Funktionen

Um eine Funktion rekursiv auzurufen kann man innerhalb des Funktionsrumpfs über die implizit deklarierte lokale Variable thisFn auf das Funktionsobjekt selber zugreifen.

var fak = fn(Number x){
	if(x>1)
		return x*thisFn(x-1);
	else
		return x;
};
out( fak(4) );    // (4*(3*(2*(1))))=24
24

Um eine Memberfunktion rekursiv aufzurufen, verwendet man am besten ein entsprechendes FnBinder: (this->thisFn)(...)

Eine andere Möglichkeit für rekursive Aufrufe ist, die Funktion in einer statische Variable zu speichern, die auch in der Funktion selber zur Verfügung steht (siehe statische Variablen).

Ausnahmebehandlung try{ ... }catch(e){...}

Die meisten Ausnahmen (inklusive einiger C++ Ausnahmen) können über try{ ... }catch(e){...} abgefangen werden (ungefähr so wie in C++). Dabei wird das Ausnahmeobjekt bei Auslösung an die Variable des catch-Blocks zugewiesen. Anders als in C++ gibt es aber immer nur genau einen catch Block der alle Ausnahmen (unabhängig deren Typs) entgegen nimmt. Möchte man nur bestimmte Typen von Ausnahmen fangen; muss man manuell eine Typprüfung im catch-Block vornehmen und alle anderen Ausnahmen neu durch throw auslösen.

try{
	1/0;
}catch(e){
	out("Caught: ",e);
}
Caught: [#EXCEPTION "Division by zero ...

Ausnahmeauslösung throw(...)

Mit throw wird eine Ausnahme ausgelöst. Der übergebene Parameter kann von einem beliebigen Typ sein und wird dann ggf. an den catch-Block übergeben.

try{
	throw( "Mein Fehler!");
}catch(e){
	out("Caught: ",e);
}
Caught: Mein Fehler!

Array-Erzeugung: [...]

Arrays lassen sich direkt durch eine Komma-getrennte Aneinanderreihung von Werten innerhalb eckiger Klammern [ ] erzeugen. Werden keine Werte angegeben, wird ein leeres Array erzeugt. (Mehr zu Operationen auf Arrays im Referenzkapitel zu Collections und Arrays.)

print_r( [1,2,"foo"] ); // formatierte Augsgabe eines Arrays.
[
	1,
	2,
	"foo"
]

Map-Erzeugung: { ... : ... }

Maps lassen sich direkt durch eine Komma-getrennte Aneinanderreihung von Schlüssel-Wert-Paaren innerhalb geschwungener Klammern { } erzeugen. Schlüssel werden von den Werten durch einen Doppelpunkt : getrennt. (Mehr zu Operationen auf Maps im Referenzkapitel zu Collections und Maps.)

Werden keine Werte angegeben, wird ein leerer Anweisungsblock und keine leere Map erzeugt. Um eine leere Map zu erzeugen, sollte new Map() verwendet werden.

print_r( {1:"foo",2:"bar","blub":3} ); // Formatierte Ausgabe einer Map.
{
	1:"foo",
	2:"bar",
	"blub":3
}

Element-Zugriff: Collection[key]

Um auf ein Element einer Map oder eines Arrays lesend oder schreibend zuzugreifen, wird der entsprechende Schlüssel in eckigen Klammern [ key ] angegeben.

var a=["a","b","c"]; // Array anlegen
var m={"foo":"bar" , 2:"blub" }; // Map anlegen
m["hubhub"]=5; // in der Map einen Wert ablegen
out( a[1]," ",m["hubhub"]," ",m["dibbel"] ); // einige Werte ausgeben
b 5 #void

Schleifen

In EScript gibt es for, while, do...while und foreach Schleifen. Zunächst einige allgemeine Punkte zu Schleifen:

Schleife: for( init ; condition ; stepExpression) ...

Es gibt einen initialen Ausdruck, der einmalig ausgeführt wird, eine Bedingung, deren boolescher Wert über die weitere Ausführung der Schleife entscheidet und einen Ausdruck, der nach jedem Schleifendurchlauf ausgeführt wird.

for(var i=0;i<3;++i)
	out("bla");
blablabla

Schleife: while( condition ) ...

Solange wie die Bedingung gilt, wird der Schleifenrumpf ausgeführt.

var i=0;
while(i<3){
	out("bla");
	++i;
}
blablabla

Schleife: do ... while(condition);

Es wird der Schleifenrumpf ausgeführt, dann die Bedingung geprüft, die entscheidet, ob der Schleifenrumpf erneut ausgeführt wird.

var i=0;
do{
	out("bla");
	++i;
}while(i<3);
blablabla

Schleife: foreach(.. as ..)

Foreach-Schleifen führen den Schleifenrumpf für alle Elemente einer Collection (Array oder Map) aus. Steht hinter dem as nur ein Identifier (ggf. mit einem var) wird in diese Variable der aktuelle Wert des Eintrags gespeichert.

var a=["a","b","c"];
foreach(a as var value)
	out(value);
abc

Werden zwei durch Komma getrennte Identifier angegeben, wird an den ersten der aktuelle Schlüssel (bei Arrays ist das der Index) zugewiesen und an den zweiten der Wert.

var a=["a","b","c"];
foreach(a as var key, var value)
	out(key,":",value," ");
0:a 1:b 2:c

Statische Variablen

Grammatik:
VarDecl ::= 'static' Identifier [...]

static deklariert eine statische Variable im umschließenden Block ( {...} ), mit den gleichen Regeln wie lokale Variablen -- jedoch mit zwei wesentlichen Unterschieden:

// static-Beispiel
{
  var a;        // führt zur Deklaration von a im umschließenden Block -- ohne Funktionen
  static b = 0; // führt zur Deklaration von b im umschließenden Block -- mit Funktionen

  static f = fn(){
	if( b<10 ){
		++b;    // Zugriff auf b; a ist nicht sichtbar
		f();    // Rekursiver Aufruf von f
	}
  };
  f();
  outln( f );
}
10

Einige Dinge zur Beachtung:

Attribute object.identifier

Objekte in EScript können Attribute haben. Dies sind Variablen, die zu dem Objekt gehören und auf die über eine Namenskonstante (Identifier) zugegriffen werden kann. Neben Attributen, die direkt in dem Objekt gespeichert sind, haben Objekte auch noch Referenzen auf Typobjekte, deren Attribute allen Instanzen des Typs zur Verfügung stehen. So hat jedes Zahlobjekt beispielsweise eine Typreferenz auf Number und hat daher Zugriff auf alle Attribute von Number. Der normale Zugriff auf Attribute erfolgt durch den Punkt-Operator (.). Links steht das Objekt und rechts der Name des gewünschten Attributs.

out( "foo".length,"\n); // Zugriff auf das Attribut "length" vom String "foo".
                        // Dies ist nicht direkt im String gespeichert, sondern
                        // im Typobjekt "String". Das Ergebnis ist eine Funktion.
out( "foo".length(),"\n" );  // Ausführung dieser Funktion
#Function
3

Man kann auch über eine Stringkonstante auf ein Attribut zugreifen (z. B. obj."attrName" entspricht obj.attrName). Dies ist insbesondere praktisch, wenn man auf Attribute zugreifen will, deren Name sich nicht als Identifier schreiben lässt (z. B. obj."+" ).

Die meisten Operatoren werden intern auf den Zugriff auf Attribute realisiert. So ist 1+2 äquivalent zu 1."+"(2). Oder auch: a[1] = "foo" entspricht a._set(1,"foo").

Erweiterbare Objekte: z. B. ExtObjects

Verschiedene Objekte (insbesondere vom Typ ExtObject oder auch vom Typ Type, aber dazu später mehr...) können zur Laufzeit mit weiteren Attributen erweitert werden. Dazu wird über den :=-Operator ein neues Attribut definiert, welches dann direkt im Objekt gespeichert wird. Der Aufruf erfolgt immer in Kombination mit dem Punkt-Operator: objekt.nameDesNeuenAttributs := initialerWert.
Danach können der Wert des Attributs normal gelesen und ihm mit dem normalen Zuweisungsoperator = neue Werte zugewiesen werden.

var e=new ExtObject();  // erzeuge eine neue Instanz von ExtObject
e.newAttr:="foo";       // neues Objektattribut anlegen
out(e.newAttr);         // lesend auf Objektattribut zugreifen
e.newAttr="bar";        // schreibend auf Objektattribut zugreifen
out(e.newAttr);         // lesend auf Objektattribut zugreifen
e.anotherAttr=5;        // Fehler: Es wird versucht, auf "anotherAttr" zuzugreifen, obwohl es nie angelegt wurde!
foobar

Funktionsattribute (Memberfunktionen)

Auch Funktionen können als Attribute einem Objekt zugewiesen werden. Wenn so eine Funktion dann ausgeführt wird, kann über this auf das jeweilige Objekt zugegriffen werden. Des Weiteren sind alle Objektattribute (und die Typattribute, dazu später mehr) direkt zugreifbar.

var e=new ExtObject();   // erzeuge eine neue Instanz von ExtObject
e.number:=1;             // Objektattribut hinzufügen
e.doSomething:=fn(){        // eine Funktion als Attribut hinzufügen
    this.foo := "bar";      // ein neues Objektattribut hinzufügen
    number++;               // direkter Zugriff auf ein Objektattribut (äquivalent zu this.number++)
    out( foo," ",number );  // Ausgabe
};
e.doSomething();
bar 2

Typen und Vererbung

Typobjekte bestimmen zum einen die Eigenschaften eines jeden Objekts (z. B. welche Attribute es initial hat) und welches C++-Objekt intern verwendet wird. Wenn man ein neues Type-Objekt erzeugt, sollte man sich zunächst überlegen, von welchem Basistyp man erben möchte (man muss von irgendetwas erben!). Meistens ist ExtObject die richtige Wahl, da dann alle Instanzen auch Attribute aufnehmen können.

Ein neuer Typ wird mit new Type( Basistyp ) erzeugt. Wenn kein Basistyp (von dem der neue Typ erbt) angegeben wird, wird ExtObject angenommen.

var Animal=new Type;         // erzeuge einen neuen Typ (erbt von ExtObject)
var Rabbit=new Type(Animal); // erzeuge einen neuen Typ (erbt von Animal)

var myRabbit=new Rabbit;     // erzeuge eine Instanz von Rabbit

Objektattribute von Typen

Type-Objekte lassen sich wie ExtObject-Objekte mit := um Objektattribute erweitern. Sobald eine Instanz eines Typs mit new erzeugt wird, wird für jedes Objektattribut des Typs ein entsprechendes Attribut im neuen Objekt zugewiesen.

var Animal=new Type;           // erzeuge einen neuen Typ (erbt von ExtObject)
Animal.name:="genericAnimal";  // Objektattribut "name" als String zu Animal hinzufügen

var a=new Animal;   // erzeuge eine Instanz von Animal
out(a.name);        // Objektattribut "name" von a ausgeben
genericAnimal

Bei der Zuweisung der Werte an die Instanz werden nur die Werte simpler Typen (Zahl,Bool,String) kopiert, wie bei einer normalen Zuweisung zu einer Variablen. Ist der Wert des Attributs beispielsweise ein Array, referenziert das Attribut der Instanz das gleiche Array (wie bei einer normalen Zuweisung zu einer Variablen)!

Leitet man von einen Typ ab (und erstellt damit einen neuen), werden zu dem Zeitpunkt auch alle Objektattribute des abgeleiteten Typs an den neuen weitergegeben.

Wird zu einem Typ ein neues Objektattribut hinzugefügt (oder geändert), nachdem dieser als Basistyp für einen neuen Typ eingesetzt wurde, wird diese Änderung nicht an den neuen, abgeleiteten Typ weitergegeben.

Erzeugt man einen Typ, der von einem Objekt erbt, das keine Objektattribute aufnehmen kann (z. B. man erbt direkt von Object), dann darf der Typ keine Objektattribute enthalten. Andernfalls wird bei der Instanziierung versucht, dem Objekt die Attribute zuzuweisen, was in diesem Fall nicht möglich ist!

var Animal=new Type;    // erzeuge einen neuen Typ (erbt von ExtObject)
Animal.canFly:=false; 	// Objektattribut "canFly" als Bool hinzufügen
Animal.friends:=[]; 	// Objektattribut "friends" als Array hinzufügen (vermutlich eine dumme Idee)

var Rabbit=new Type(Animal); // erzeuge einen neuen Typ (erbt von Animal)
Rabbit.name:=void;           // leeres Objektattribut "name" hinzufügen

Animal.weight:=0;  // Objektattribut "weight" hinzufügen
                   // Achtung: Der Typ Rabbit ist aber schon angelegt, bekommt davon also nichts mit!

var hubert=new Rabbit;     // erzeuge eine Instanz von Rabbit
var emma=new Rabbit;       // erzeuge eine Instanz von Rabbit

hubert.name = "Hubert"; // weise dem Objektattribut "name" von hubert einen Wert zu
emma.name = "Emma";     // weise dem Objektattribut "name" von emma einen Wert zu
emma.canFly = true;     // Emma kann fliegen!

hubert.friends += "HubDiWub"; // Füge einen Eintrag in das Array "friends" hinzu.
                              // Achtung: Da "friends aber vom Typ Array ist und Arrays call-by-reference übergeben werden,
                              // teilt sich hubert das "friends"-Array mit Animal, Rabbit und auch emma!

// hubert.weight = 10;  // Fehler: "weight" ist kein Objektattribut von hubert!

out( hubert.name, " ", hubert.canFly, " friends:",hubert.friends.implode(",") ,"\n" ); // Ausgabe
out( emma.name, " ", emma.canFly, " friends:",emma.friends.implode(",") ,"\n" );       // Ausgabe
Hubert false friends: HubDiWub
Emma true friends: HubDiWub

Typattribute

Neben den Objektattributen können Typen auch Typattribute enthalten. Diese Attribute sind von jeder Instanz zugreifbar, sie werden jedoch nicht als Attribut in jeder Instanz angelegt sondern verbleiben im Typ-Objekt. Man kann sie sich daher in etwa wie Klassenvariablen (Schlüsselwort static) in C++ vorstellen. Neben dem Einsatz für typenübergreifende (statische) Variablen, lassen sie sich vor allem zum Speichern von Funktionen verwenden. Meistens braucht man nicht für jede Instanz eines Typs ein eigenes Objektattribut für jede Funktion, sondern es reicht aus, wenn die Funktion über den jeweiligen Typ zugreifbar ist. So wird z. B. nicht für jeden einzelnen String bei seiner Erzeugung ein Attribut für die length-Funktion angelegt, sondern diese ist nur einmal im String-Typobjekt als Typattribut gespeichert. Da jeder String auf den Typ String verweist, ist der Zugriff dennoch möglich: "foo".length(). Ein Typattribut ist auch von allen abgeleiteten Typen zugreifbar. Fügt man beispielsweise ein Typattribut zum Typobjekt Object hinzu, ist dieses beispielsweise von jedem Number-Objekt aus zugreifbar.

Um ein Typattribut zu einem Typobjekt hinzuzufügen, wird der ::=-Operator verwendet: typeObj.typeAttr::=initialValue;.

var Animal=new Type;           // erzeuge einen neuen Typ (erbt von ExtObject)
Animal.name:="genericAnimal";  // Objektattribut hinzufügen
Animal.printCounter::=0;       // Typattribut hinzufügen (von allen Instanzen zugreifbar)

Animal.print::=fn(){           // Funktion als Typattribut hinzufügen
	printCounter++;            // Zugriff auf Typattribut
	out(printCounter,".) My name is ",name,"\n"); // Zugriff auf Typattribut "printCounter" und Objektattribut "name"
};

var a=new Animal;   // erzeuge eine Instanz von Animal
a.name="Pubert";
var b=new Animal;   // erzeuge eine Instanz von Animal
b.name="Moehk";
a.print();
b.print();
1.) My name is Pubert
2.) My name is Moehk

Konstruktoren

Bei der Instanziierung eines Typs wird über den new-Operator der Konstruktor aufgerufen. Um einen eigenen Konstruktor zu definieren, kann das _constructor-Attribut des Typobjekts definiert werden. (Sinnvollerweise als Typattribut und nicht als Objektattribut; eine eigene Referenz auf den Konstruktor in jeder Instanz macht in den meisten Fällen keinen Sinn.)

var Animal=new Type;           // erzeuge einen neuen Typ (erbt von ExtObject)
Animal.name:=void;             // Leeres Objektattribut hinzufügen

Animal._constructor::=fn(n){   // Konstruktor als Typattribut hinzufügen
	name=n;
};

Animal.print::=fn(){           // Funktion als Typattribut hinzufügen
	out("My name is ",name,"\n");
};
var a=new Animal("Pubert");    // erzeuge eine Instanz von Animal
a.print();
My name is Pubert

Superkonstruktoren

Bevor der Rumpf eines Konstruktors aufgerufen wird, wird immer jeweils der Konstruktor der Basisklasse (=Superkonstruktor) rekursiv aufgerufen. Um an den jeweiligen Superkonstruktor Parameter übergeben zu können, können entsprechende Ausdrücke bei der Definition der Konstruktorfunktion nach den Parametern hinter einem Punktoperator angegeben werden.
fn( konstruktorParameter )@(super( superkonstruktorParameter )) { ... }

var Animal = new Type;           // erzeuge einen neuen Typ (erbt von ExtObject)
Animal.name := void;             // Leeres Objektattribut hinzufügen
Animal.canFly := void;           // Leeres Objektattribut hinzufügen
Animal._constructor ::= fn(n,f){ // Konstruktor als Typattribut hinzufügen
	name = n;
	canFly = f;
	out("Animal created\n");
};
Animal.print ::= fn(){           // Funktion als Typattribut hinzufügen
	out("My name is ",name,"and I can ", (canFly?"fly":"not fly") ,".\n");
};

var Dragon = new Type(Animal);   // Dragon erbt von Animal
Dragon._constructor ::= fn(n)@(super(n+" the Great",true)){ // Konstruktor ruft Superkonstruktor mit zwei Parametern auf
	out("Dragon created\n");
};

var a = new Dragon("Pubert");    // erzeuge eine Instanz von Dragon
a.print();
Animal created
Dragon created
My name is Pubert the Great and I can fly.

Es wird als erstes der Konstruktor desjenigen Basistyps aufgerufen, dessen Konstruktor ein C++-Funktionsobjekt vom Typ Function und nicht mehr vom Typ UserFunction ist. Dieser muss dann das eigentliche C++-(EScript)-Objekt zurück liefern.

Eigenschaften von Attributen

Neben der Unterscheidung von Typattributen und Objektattributen, gibt es noch mehrere anpassbare Attributeigenschaften (seit Version 0.6.0). Um diese zu definieren, kann zwischen dem Namen des Attributes und dem Attributerzeugungsoperator (:= oder ::=) eine Liste von Eigenschaftskonstanten angegeben werden: Object.AttributName @(Eigenschaft1,Eigenschaft2, ...) := Wert. Es können dabei mehrere Eigenschaften kombiniert werden, wobei sich unsinnige Kombinationen ergeben können, die dann eine Warnung verursachen (z.B. @(private,public) ). Es folgt eine Übersicht über die wichtigsten Eigenschaften.

@(const)

Die Eigenschaft @(const) verhindert, dass einem Attribut ein neues Objekt zugeordnet wird. Der Wert des gespeicherten Objektes kann sich jedoch durchaus ändern. Damit entspricht es in C++ einem const-Pointer (und nicht einem const-Wert). Beim Versuch einen neues Objekt zuzuweisen, wird eine Ausnahme geworfen.

var a = new ExtObject;
a.v @(const) := 1; // Konstantes Objektattribut anlegen.

a.v += 1; // Erlaubt, das Objekt bleibt gleich und nur sein Wert ändert sich;

a.v = a.v + 1; // Ausnahme wird geworfen beim Versuch der Zuweisung zu einem konstanen Attribut.

@(init)

Die Eigenschaft @(init) erlaubt die Initialisierung von Objektattributen mit Instanzen eines Typs oder dem Rückgabewert einer beliebigen Funktion (oder FnBinder oder anderem ausführbaren Objekt). Dazu ein Beispiel, bei dem jeder Instanz eines Objektes ein Array als Objektattribut hinzugefügt werden soll:

// Möglichkeit 1: Im Konstruktor
var A = new Type;
A.arr := void; // leeres Objektattribut anlegen
A._constructor ::= fn(){
	arr = []; // Array erzeugen und zuweisen
};


// Möglichkeit 2: FALSCH!!!!!!!!!!
var B = new Type;
B.arr := []; // Einzelnes Array-Objekt als Objektattribut anlegen
// Dies führt dazu, dass zwar alle Instanzen von B ein eigenes Objektattribut
// 'arr' besitzen. Alle teilen sich jedoch das selbe Array!!!!!!


// Möglichkeit 2: @(init) mit Typ
var C = new Type;
C.arr @(init) := Array; // Attribut wird mit einem Type-Objekt initialisiert.
// Jede Instanz von C bekommt nun eine eigene Instanz von Array als Objektattribut 'arr'


// Möglichkeit 2: @(init) mit Funktion
var D = new Type;
D.arr @(init) := fn(){ return []; }; // Attribut wird mit einer Funktion initialisiert.
// Für jede Instanz von D wird die Funktion aufgerufen und ihr Rückgabewert im Objektattribut 
// 'arr' gespeichert.

@(override)

Die Eigenschaft @(override) führt dazu, dass das Attribut bereits existieren muss (im Objekt selber oder in einem der geerbten Typen), wenn es angelegt wird. Existiert noch kein Attribut gleichen Namens, wird eine Warnung ausgegeben; die Attributerstellung wird jedoch ausgeführt. Diese Eigenschaft ist nützlich um auf Tippfehler oder Änderungen in den Methodennamen einer Basisklasse hingewiesen zu werden und sollte daher an entsprechenden Stellen auch eingesetzt werden ;-)

var A = new Type;
A.m ::= fn(){	out("foo!"); };

var B = new Type(A);
B.m @(override) ::= fn(){	out("bar!"); }; // Ok.
B.m2 @(override) ::= fn(){	out("grrr!"); }; // Warnung, da 'm2' nicht existiert.
B.m2 @(override) ::= fn(){	out("blubb!"); }; // Jetzt ok, da 'm2' trotz Warnung in der letzten Zeile angelegt wurde.

@(private)

Die Eigenschaft @(private) schränkt den Zugriff auf ein Memberattribut so ein, dass nur noch von Membermethoden des jeweiligen Objektes darauf zugegriffen werden kann. Zugriffe von ausserhalb führen zu einer Warnung und werden nicht ausgeführt.

@(private) verhält sich anders als z. B. das private in C++. In C++ schränkt es den Zugriff auf Memberfunktionen des gleichen Typs ein; in EScript wird der Zugriff auf das jeweilige Objekt eingeschränkt. Ein Zugriff auf geerbte, private Attribute ist möglich (in C++ nicht). Ein Zugriff auf private Attribute eines anderen Objektes des exakt gleichen Typs ist nicht möglich (in C++ jedoch schon).
var A = new Type;
A.v1 @(private) := 1;  // privates Objektattribut anlegen
A.m1 @(private) ::= fn(){ out("m1:",v1,"\n"); }; // privates Typattribut anlegen
A.m2 ::= fn(obj){ out("m2:",obj.v1,"\n"); }; 
var B = new Type(A); 
var b = new B();
b.m3 := fn(){	m1();  }; // aus öffentlicher Funktion, geerbte, private funktion aufrufen...ok!

out( b.v1 ); // Warnung wegen Zugriffsverletzung; Ausgabe 'void'
b.m1(); // Warnung wegen Zugriffsverletzung. Keine Ausgabe.
b.m3(); // Ok. Ausgabe: 'm1:1'.
(b->fn(){m1();})(); // Ok (Da das FnBinder als Memberfunktion wirkt. Siehe FnBinders) Ausgabe: 'm1:1'.
b.m2(b); // OK. Ausgabe: 'm2:1'.
b.m2(new B()); // Warnung wegen Zugriffsverletzung. Keine Ausgabe.

@(type)

Die Eigenschaft @(type) sorgt für die Anlegung eines Typattributes (anstatt eines Objektattributes). Damit ist es nichts anderes als eine alternative Schreibweise für den ::=-Operator. Der Einfachheit halber sollte auch der ::=-Operator vorgezogen werden. Ich habe die @(type)-Eigenschaft an dieser Stelle nur erwähnt, um deutlich zu machen, dass der ::=-Operator nur eine syntaktische Kurzschreibweise für diese Eigenschaft darstellt.

var A = new Type;
A.t1 ::= "dies ist ein Typattribut";
A.t2 @(type) := "dies ist auch ein Typattribut";

FnBinder

Ein FnBinder ist die Kombination eines Funktionsobjektes und gebundener Parameterwerte und/oder gebundenem, aufrufenden Objekt.

Binden des aufrufenden Objektes (Delegate)

Ein FnBinder, mit einem gebundenen, aufrufenden Objekt, wird durch den ->-Operator erzeugt: object -> function. Die Funktion des FnBinders wird wie eine Attributsfunktion (oder auch Memberfunktion) des Objekts ausgeführt; d. h. innerhalb der Funktion kann man z. B. mit this auf das Objekt des FnBinders zugreifen. Einsetzen lassen sich FnBinders beispielsweise bei der Implementierung von Listenern:

var a = new ExtObject;          // ExtObject erzeugen
a.name := "foo";                // Objektattribut hinzufügen
a.wakeUp := fn(){               // Funktion hinzufügen
	out(name," wakes up");
};

// Einfaches Beispiel
var delegate = a -> fn(){      // ein FnBinder erzeugen
	out(name,"\n");
};
delegate();                   // FnBinder ausführen

// Listener Beispiel
var listener = [];              // Array anlegen
listener += a -> a.wakeUp;     // FnBinder erzeugen und zu Listener-Array hinzufügen
// ...

foreach(listener as var l)    // jeden Listener ausführen
    l();                      // Hier brauchen wir den Attributnamen der aufgerufenen Funktion (wenn sie denn einen hat)
                              // nicht zu kennen. Sowohl Objekt als auch Funktion sind ja im FnBinder gespeichert.
foo
foo wakes up

Binden von Parameterwerten

Ein FnBinder, mit gebundenen Parameterwerte, wird durch den =>-Operator erzeugt: Array mit Werten => function. Die Werte werden an die ersten Parameter gebunden; die restlichen Parameter bleiben frei.

var f = fn( a,b,c){
	outln( "a:", a, " b:", b, " c:", c );
};
var b1 = [4] => f;
var b2 = [4,17] => f;
var b3 = [42] => b2;

f(1,2,3);
b1(1,2);
b2(1);
b3();

a:1 b:2 c:3
a:4 b:1 c:2
a:4 b:17 c:1
a:4 b:17 c:42

Wenn man mit einem FnBinder sowohl ein Objekt, als auch Parameter binden möchte, sollte das Objekt innen (rechts) stehen. Z. B. [1,2,3] => obj->fn(p1,p2,p3){}.

Globale Variablen

Alle globalen Variablen werden durch Attribute eines speziellen GLOBALS-Objekts abgebildet. Globale Variablen sind von überall direkt zugreifbar, wie z. B. die Funktion out. Um neue globale Variablen zu deklarieren, können sie als Objektattribute zu GLOBALS hinzugefügt werden. GLOBALS selbst ist eine globale Variable und kann so von überall erreicht werden

GLOBALS.doSomething:=fn(){   // neue globale Funktion deklarieren
	out("Dumdidum...\n");
};
var f=fn(){
    doSomething();           // doSomething ist global, daher auch in allen Funktionen aufrufbar.
};
f();
Dumdidum...

Neue globale Variablen sollten nur in Ausnahmefällen deklariert werden! Statische Variablen sind eine mögliche Alternative.

Spezielle Konstanten

Während des Parsens von Skriptdateien werden vom Tokenizer einige Ausdrücke durch andere Werte ersetzt (wie beim C-Präprozessor):

__FILE__ durch den Dateinamen des aktuellen Skripts als String,
__DIR__ durch das Verzeichnis des aktuellen Skripts als String,
__LINE__ durch die aktuelle Zeilennummer als Zahl.

Das ist vor allem praktisch fürs Debugging und um Skriptdateien aus dem gleichen Verzeichnis zu laden.

Kurzreferenz

StdLib

In der StdLib sind einige Funktionen im globalen Namensraum definiert. Im Folgenden eine kurze Übersicht über die wichtigsten.

Object eval(String expression)

Mit eval wird EScript-Code compiliert, ausgeführt und das Ergebnis zurückgegeben. Fehler, die beim Parsen auftreten können (z. B. Syntaxfehler), lösen eine Ausnahme aus.

var s="1+2;";
var result;
try{
   result = eval(s);    // Anweisung compilieren, ausführen und Ergebnis ausgeben
}catch(e){                      // mögliche Fehler abfangen
   out(e);
}
out(result);
3

Object load(String filename) / Object loadOnce(String filename)

load und loadOnce laden eine EScript-Datei und führen sie aus. loadOnce tut dies jedoch nur einmal für jede Datei und gibt bei wiederholtem Aufruf lediglich void zurück. Wird die geladene Datei mit einem return-Statement beendet, liefern die Funktionen den entsprechenden Rückgabewert zurück. Kann die Datei nicht geladen werden oder werden Fehler entdeckt, wird eine Ausnahme ausgelöst.

void out(...) / void outln(...) / void print_r(...)

out gibt alle übergebenen Parameter als String aus. outln gibt alle übergebenen Parameter als String und einem abschließenden Zeilenumbruch aus. print_r gibt ebenfalls alle Parameter als String aus, jedoch werden Array- und Map-Objekte formatiert ausgegeben.

Number clock()

clock gibt die Anzahl der Sekunden seit Programmstart zurück.

Number time() / Map getDate( [Number time] )

time gibt die aktuelle Zeit als fortlaufende Zahl zurück. getDate wandelt diese Zahl in eine Map mit entsprechenden Angaben um.

print_r( getDate() );
{
        [hours] : 19,
        [isdst] : 1,
        [mday] : 19,
        [minutes] : 56,
        [mon] : 7,
        [seconds] : 32,
        [wday] : 1,
        [yday] : 199,
        [year] : 2010
}

String chr(Number ascii)

Gibt einen String mit einem Zeichen entsprechend des übergebenen ASCII-Codes zurück.

for(var i=65;i<=90;++i)
    out(chr(i));
ABCDEFGHIJKLMNOPQRSTUVWXYZ

String getOS()

Gibt einen String entsprechend des verwendeten Betriebssystems zurück. Mögliche Werte sind "WINDOWS", "MAC OS", "LINUX", "UNIX" oder "UNKNOWN".

String toJSON( String|Number|Bool|Array|Map|Void value[, Bool formatted=true] )

toJSON wandelt Standardtypen (String, Number, Bool, Array, Map und Void) in eine JSON-formatierte Zeichenkette um. void wird dabei zu null. Wenn der Parameter formatted auf true gesetzt ist, wird die Zeichenkette formatiert (ggf. mehrzeilig mit Einrückung).

Object parseJSON(String)

toJSON wandelt eine JSON-formatierte Zeichenkette wieder in Standardobjekte um.

IO.getFileContent ...

TODO...

Object

Wie bereits erwähnt, erben alle Objekte aus denen man in EScript zugriff hat von der Klasse Object. Die Memberfunktionen dieser Klasse sind daher auch von jedem Objekt aufrufbar.

Konstruktor: new Object()

Erzeugt ein neue Instanz von Object.

Ein einfaches Objekt benötigt man nur in seltenen Fällen. Meistens möchte man ein Objekt, dass man dynamisch um Attribute erweitern kann. Dafür sollte man ein ExtObject verwenden.

Explizite Typumwandlung (toBool(), toNumber(), toString())

Jedes Objekt unterstützt die explizite Typumwandlung in die Standardtypen Bool,Number und String über die Funktionen toBool(), toNumber() und toString(). Die Konvertierung verläuft nach den oben genannten Regeln.

An dieser Stelle nocheinmal der Hinweis auf einen beliebten Fehler: Die einzigen Objekte, die bei toBool() den Wert false zurückliefern sind der Wert false und void. Auch die Zahl 0 liefert true!

Vergleichsoperationen ( ==, !=, ===, !==)

Die Vergleichsoperator == überprüft zwei Werte auf Gleichheit und gibt das Ergebnis als Bool zurück. Ist die linke Seite des Vergleichs ein Objekt eines einfachen (call by reference) Typs wird bei der rechte Wert mit impliziter Typumwandlung in den Wert des linken umgewandelt. Dadurch liefert z.B. 17 == "17" und true == 0 den Wert true.
Andere Objekte können den Operator anders implementieren. Arrays überprüfen bespielsweise ob das andere Objekt auch ein Array mit den gleichen (im Sinne von "==") Objekten ist.
Der Operator != liefert standardmäßig den negierten Wert des ==-Operators zurück.

Der Operator === überprüft ob die beiden Objekte identisch sind. Bei einfachen (call by reference) Typen (toBool(), toNumber(), toString()) bedeutet dies, dass sowohl der Wert als auch der Typ übereinstimmen muss. Dadurch liefert z.B. 17 === "17" und true === 0 hier den Wert false.
Bei anderen Typen (z.B. Array, ExtObject ...) wird überprüft ob es sich tatsächlich um die selben Objekte handelt. Der Operator !== liefert den negierten Wert des ===-Operators zurück.

Um zu überprüfen, ob ein Wert void ist, sollte grundsätzlich der ===-Operator genutzt werden. Z.B.:if( myVar === void) doSomething();
Um aus C++ zwei Objekte zu vergleichen sollte immer EScript::isEqual(runtime,leftObject,rightObject) (oder das entsprechende isIdentical(...)) verwendet werden. Um aus C++ für einen eigenen Typ eine eigene Vergleichsoperation zu definieren, sollte die Memberfunktion rt_equals( other ) überschrieben werden. In EScript kann ein eigener Vergleichsoperation durch überschreiben des "=="-Attributs erreicht werden. Das "!="-Attribut muss nicht zusätzlich implementiert werden.
TODO!
EScript::isEqual(rt,left,right) -> left.'=='
 '=='   -> rt_equals( other )
 '!=' -> ! EScript::isEqual(rt,caller,right)

EScript::isIdentical(rt,left,right) -> left.'==='
 '===' -> rt_identical( other )
 '!==' -> ! EScript::isIdentical(rt,caller,right)

Operator Object ---|> Type

Der ---|>-Operator (isA-Operator) liefert true wenn der Wert links vom rechts angegeben Typ ist. Die Vererbung der Typen wird dabei beachtet, d.h. auch wenn der Typ des Objekts vom angegebenen Typ erbt, liefert der Operator true.

Um zu überprüfen, ob ein Wert von einem bestimmten Typ ist (ohne Einbeziehung der Vererbung) kann man über die getType() Funktion den Typ abfragen und dann mit dem gewünschten Typ vergleichen.
out( 3 ---> Number ); // 3 is of Type Number or inherits from Number ... true
out( 3.getType() == Number  ); // check if the type of 3 is Number (and not inherited from Number) ... true

var a=new Array();
out( a ---> Collection ); // a is of Type Array, Array inherits from Collection ... true

out( "foo" ---|> String  ); // The type of "foo" is String ... true
out( "foo".getType() ---|> String  ); // The type of "foo" is String, String is of type Type (and not String) ...  false
out( "foo".getType() ---|> Type  ); // The type of "foo" is String, String is of type Type ... true

out( 3 ---|> "foo"  ); // "foo" is no Type ... ERROR

Object.clone()

Alle in EScript integrierten Typen unterstützen die clone()-Funktio, die eine Kopie des Objektes zurück gibt.

Wird eine Collection (also ein Array oder eine Map) mittels clone kopiert, wird nur eine flache Kopie angelegt. D.h. enthält die Collection auch Werte mit nicht simple Typen (wie z.B. weitere Collections) werden diese nur referenziert und nicht rekursiv kopiert.

// create a map with a simple value and a non simple value
var m={ "simple" : "foo",
		"complex" : [ 1,2,3 ] };

var m2=m.clone(); // m2 is a flat copy of m1

m2["simple"] = "bar";
m2["complex"] += 4; // this does also modify m1, as "complex" references
                    // the same Array

out("m1:");
print_r(m1);
out("m2:");
print_r(m2);
m1:
{ "simple : "foo" , [ 1,2,3,4] }
m2:
{ "simple : "bar" , [ 1,2,3,4] }

Bool

Object-Member

	TODO!

Number

Object-Member

	TODO!

String

Object-Member

	TODO!

Collection

Object-Member

	TODO!

ExtObject

ExtObject.clone()

Das Zuweisen der Attribute an das neue Objekt geschieht entsprechend der Call-By-Value oder Call-By-Reference Eigenschaft des entsprechenden Wertes. Ist ein Attribut beispielsweise ein Array, so wird nachher vom Klon auf das gleiche Element referenziert. Da dies nicht immer erwünscht ist, empfiehlt sich hier das Überschreiben der code-methode:

\\ TODO Beispiel erstmal nur aus Testcase übernommen; muss noch überarbeitet werden!
var A=new Type(ExtObject);   // A erbt von ExtObject
A.m1:=1;                     // m1 enthält einen call-by-value Wert, kann also so initialisiert werden
A.m2:=void;                  // m2 soll einen call-by-reference Wert enthalten, wird also zunächst leer initialisiert...
A._constructor ::=fn(){
	m2=[];                   // ... und dann im Konstruktor zugewiesen
};

// ---|> ExtObject
A.clone ::=fn(){
	var n=(this -> (ExtObject.clone))();  // zunächst die clone-Methode von ExtObject auf dem aktuellen Objekt aufrufen
	n.m2=m2.clone();                      // und von hand den call-by-reference Wert clonen.
	return n;
};
var a=new A();
var b=a.clone();

a.m1++;
a.m2+="x";

print_r(a._getAttributes());
print_r(b._getAttributes());

Array

Object-Member

self Array.sort( [comparatorFunction] )

Man kann Array.sort eine Funktion mit 2 Parametern geben, die für jeden Vergleich aufgerufen wird.

// Einfaches Beispiel: (normales Sortieren)

var array=[3,23,7,3,100,1,35];
array.sort( fn(a,b) { return a<b; } );

// == [1,3,3,7,23,35,100]

// Komplexeres Beispiel: (Nach Abstand zum Wert 59 sortieren.)

var s=new ExtObject();
s.base := 59;
array.sort( s->fn(a,b) {return (a-base).abs()<(b-base).abs(); } );

// == [35,23,100,7,3,3,1]

Intern ist hier ein QuickSort-Algorithmus implementiert.

Map

Object-Member


	TODO!

EScript in C++ erweitern

	TODO!