Änderungen von BOB+2 gegenüber BOB+
Allgemeines
- Speicherverwaltung mit Referenzzählung
- Stack und Länge von Symbolen sind nur durch den Speicherplatz begrenzt
- Es gibt keine explizite Begrenzung der Schachtelungstiefe von Schleifen mehr.
- BOB+2 ist eine gekapselte virtuelle Maschine, von der in einem Prozess mehrere
unabhängige Instanzen existieren können. Die Kommandozeilenversion bp2.exe ist lediglich
eine Minimalanwendung, die Startparameter von der Kommandozeile übernimmt und Ein-
und Ausgaben über die Konsole abwickelt.
- Alle Textausgaben erfolgen in BOB+2 über eine abstrakte Print-Schnittstelle, die
von jeder Applikation nach Belieben implementiert werden kann. Die BOB+2-Bibliothek
bringt vordefinierte Implementierungen für Konsolenausgaben und - in der 32-Bit-Version
- für Ausgaben über einen installierbaren Callback mit. Normale Ausgaben sind von
Debug- und Fehlerausgaben unterscheidbar, was die Einbindung in Entwicklungsumgbungen
erleichtern soll.
- zusätzlicher Zeilenende-Kommentar, mit #! eingeleitet
- Pseudo-Schlüsselwörter TRON, TROFF und TRSTEP für Debugging (eingeführt mit BOB+
1.1a),
Wartefunktion für Step ist definier- und installierbar (z.B. für Umgebungen ohne
Konsole)
- Neues Schlüsselwort var
... dient der Definition lokaler Variablen;
ersetzt (oder ergänzt) Variablen-Definitionsliste im Funktionskopf (alte Form weiter
möglich)
Syntax: <varstatement> := "var" <identifier>
[ "=" <expr> ] { "," <identifier>} [ "=" <expr> ]} ";"
.
Das var-Schlüsselwort leitet eine Definitions-Anweisung ein und kann stehen, wo
eine normale Anweisung stehen kann.
<varstatement> ist auch an Stelle des Initialisierungsausdrucks einer for-Anweisung
zulässig.
Der Gültigkeitsbereich ist immer der der definierenden Funktion (nicht der Block!!).
- var und
null vor Deklarationen:
Dort, wo in C++ der Rückgabetyp einer Funktion (auch Member-Funktion) steht, kann
optional var oder
null stehen. In Member-Variablen-Deklarationen ist nur
var zulässig. Die Verwendung beider Schlüsselwörter ist optional
und ohne tatsächliche Bedeutung. Sie können aber die Lesbarkeit verbessern und sind
im Zusammenhang mit Doxygen von Vorteil
- Neues Schlüsselwort function:
... erlaubt die Definition einer Funktion als Literal. Damit können insbesondere
anonyme Funktionen als Rechtswert eines (Zuweisungs-)Ausdrucks definiert werden.
Syntax
"function" "(" [<argumentlist>]
[ ";" <localvarlist>] ")" "{" { <statement>} "}" .
Achtung: Das Konstrukt wird wie ein Literal behandelt und kann überall dort
vorkommen, wo ein Literal zulässig ist. Somit kann - sofern es als Abschluss einer
Anweisung notiert wird - ein nachfolgendes Semikolon erforderlich sein (trotz der
schließenden Klammer des Anweisungsblocks).
- Reservierter Bezeichner self:
Eine Funktion (oder auch Methode eines Objekts) kann sich mit "self" "(" [<argumentlist>] ")" selbst
aufrufen. Dies ist vor allem für anonyme Funktionen gedacht, die sonst keinen Möglichkeit
zum rekursiven Aufruf hätten.
self kann auch innerhalb von Member-Funktionen verwendet werden - im Unterschied
zu "normalen" Aufruden wird hier keine Suche über den Methodennamen durchgeführt,
sondern es erfolgt eine direkte Bindung des Codes. Dadurch können bei rekursiven
Aufrufen Geschwindigkeitsvorteile erzielt werden.
- Schlüsselwort static kann auch im
Rumpf einer Funktion verwendet werden und definiert dann eine oder mehrere lokale
statische Variable(n).
Syntax:
"static" [var] <identifier> [ "=" <litexpr>
] { "," <identifier>}
[ "=" <litexpr> ]} ";"
- Erweiterungsschnittstelle für in C++-DLLs implementierte Funktionen und Klassen
- VM hat C-API zur Verwendung in Nicht-C++-Umgebungen
- Ausnahmebehandlung (try-catch) --> neue Schlüsselwörter try, catch, throw
- Fallunterscheidung (switch-case-default) --> neue Schlüsselwörter switch, case,
default
- Unterstützung für Quelltext-Debugging:
- VM hat Funktion setGenerateLineInfo zum Einschalten der Zeileninformationen
- Wenn eingeschaltet generiert der Compiler Zeileninformationen (eigener Opcode)
- Interpreter ruft auf VM Funktion notifySourceLineChanged zurück, die ihrerseits
einen installierbaren Callback auftuft
- Kommandozeilenversion hat Option -l zum Einschalten
Literale
- Array-Literale:
Arrays bzw. Vektoren können als Literale initialisiert werden, z.B:
myArray = [1,2,3];
otherArray=["ich","du", [1,2,3],"er",'\n',0x23]; // gemischte Typen, eingebettetes
Array
- Dictionary-Literale:
myDict = { "Name" : "Müller", "Alter" : 52, "Kinder" : ["Anne","Emil"]};
allgemein: { <key> : <value> [, ... ] }; <key> ist immer ein String,
<value> beliebig
- Funktions-Literale: siehe oben (Schlüsselwort
function)
- Escape-Codes:
Zusätzlich zu den bereits in BOB+ unterstützten Escape-Codes werden beliebige Zeichencodes
erkannt, die nach einem Backslash als normale Integer-Literale notiert werden.
Beispiele: "Die Zeichenkette \7kann piepen.", 'Zeilenumbruch:\0xD\0xA", '\0xFF'
- Zeichenkettenliterale können wie in C++verkettet werden:
s = "Dieser String "
"gehört zusammen."
- Literale Ausdrücke:
Mit Literalen können Ausdrücke gebildet werden (im Prinzip wie auch sonst), wobei
die Operanden selbst Literale sein müssen.
Ausdrücke rechts von Initialisierungszuweisungen (static var, static member), #defvar und
#deflit-Instruktionen werden automatisch als Literalausdrücke erkannt. An anderen
Stellen kann ein Ausdruck mit #(<litexpr>)
explizit als Literal gekennzeichnet werden.
- Vordefinierte Literale:
Es gibt die vordefinierten Literale UNICODE, WIN32, WINCE und MSDOS, die jeweils
den Wert 1 haben, wenn beim Kompilieren die jeweilige Gegebenheit zutrifft und 0,
wenn nicht.
Verarbeitungsanweisungen
- können auch in Klassendeklarationen und Blöcken innerhalb einer Funktion vorkommen
- sinnvoll für #include
- #include ist neu und funktioniert im Prinzip wie in C/C++
- Datei wird zur Übersetzungszeit an Stelle der #include-Anweisung eingebunden
- #include <myincfile.inc> ... sucht Datei im aktuellenVerzeichnis sowie Pfad
aus BPPINC-Umgebungsvariable
- #include "myincfile.inc" ... sucht Datei im aktuellen Verzeichnis
- In beiden Varianten sind relative Pfade zugelassen
- #use "mylib.bpm" sucht im durch BPPLIB
angegebenen Pfad, nicht mehr in PATH
- #import "mydll.dll"
sucht nach DLL mit"Native"-Modul in PATH
- #deflit <identifier> <litexpr>
definiert ein benanntes Literal
- #literal <identifier> <litexpr>
definiert ein benanntes Literal, sofern es nicht bereits definiert ist
- #undeflit <identifier>
hebt die Definition eines benannten Literals auf
-
Eingebaute
Datentypen
- Fließkommazahlen können den internen Typ double (8 Bytes) oder float (4 Bytes)
haben. Standard ist double.
- Zusätzliche Referenz-Datentypen:
- Buffer : Puffer für beliebige Binärdaten
- CharBuffer : Puffer für eine Menge von Textzeichen - für schnelle Zeichenkettenmanipulationen
- Dictionary : Container für Schlüssel-Wert-Paare
- Referenz-Datentypen werden normalerweise automatisch - eben per Referenzzählung
- verwaltet. Sie können explizit davon ausgenommen werden, um sie "zu Fuß" wieder
wegzuräumen.
Vordefinierte Operatoren
- Es gibt einen speziellen Referenz-Zuweisungsoperator
=>
Er kann mit allen Referenztypen verwendet werden schließt die zugewiesene Objektreferenz
von automatischen Verwaltung über die Referenzzählung aus. Er dient in erster Linie
der Lösung des Problems von zyklischen Referenzen - dazu ein einfaches Beispiel:
Es sei eine Hierarchie von gleichartigen Objekten gegeben. Ein Objekt (Element,
Knoten im Hierarchiebaum) hat eine Menge (z.B. Liste oder Vektor) von Kindelementen.
Fügt man ein neues Element als Kind eines anderen ein, so wird der Referenzzähler
des erhöht (auf eins gesetzt, falls das Element neu erzeugt wurde). Wird nun der
Wurzelknoten gelöscht, so gibt er auch seine Kindknoten frei, d.h. vermindert deren
Referenzzähler um eins. Werden sie nirgendwo sonst verwendet, sterben auch
sie usw.
Anders sähe es aus, wenn die Kindknoten immer einen Rückwärtsverweis auf den Elternknoten
hätten. Dann würde beim Setzen dieses Verweises mittels einer normalen Zuweisung
der Referenzzähler des Elternobjekts erhöht und es könnte niemals freigegeben werden,
weil ja immer mindestens noch eine Referenz darauf (nämlich die des Kindknotens)
existiert. Mit zusätzlichen Operator wird dieses Problem umgangen, indem bei der
Zuweisung der Referenzzähler eben nicht erhöht wird.
Eine weitere - wohl wesentlich seltenere - Anwendung des Operators ist es, ein Objekt
von vornherein von der Referenzzählung auszunehmen. Das kann dann nötig sein, wenn
es knappe Systemressourcen beansprucht, die unbedingt zu einem bestimmten Zeitpunkt
freigegeben werden müssen. Ein von vornherein (uns ausschließlich) mit => zugewiesenes
Objekt kann mit dem delete-Operator
explizit gelöscht werden.
- Der Operator delete dient ausschließlich
der Freigabe von Objekten, die explizit von der Referenzzählung ausgeschlossen wurden.
Für alle anderen Objekte ist er zwar syntaktisch zugelassen aber wirkungslos. Anders
als in BOB+ kann er auf alle dynamischen Objekte - nicht nur auf Klasseninstanzen
angewendet werden. Er ersetzt damit die Freigabefunktion
free.
- Alle arithmetischen Operatoren können mit Operanden der Typen int, float oder double
verwendet werden. Bei unterschiedlichen Typen erfolgt eine implizite Umwandlung
in den größten Typ. Dies gilt auch für den Modulo-Operator (%), der bisher nur für
Ganze Zahlen verwendbar war.
- Hinzugekommen sind kombinierte Zuweisungsoperatoren für Bit-Operationen. also
|=, &=, ~=, ^=, <<= und
>>=. Diese Operatoren sind -
wie alle Bit-Operatoren - auf Integer-Operanden anwendbar
- Operator []: Der Operator ist auf Vektoren, Strings, Buffer, CharBuffer, Dictionarys
und Objekte anwendbar.
Bei Dictionarys ist das Argument vom Typ String, bei Objekten hängt der Argumenttyp
von der Implementierung des überladenen Operators ab, für alle anderen Typen wird
ein Index vom Typ Int verwendet.
Funktionen
- Funktionen können wie bisher mit mehr Parametern aufgerufen werden, als formale
Parameter definiert wurden. Anders als bisher (d.h. in BOB und BOB+) werden in BOB+2
nicht die letzten sondern die ersten übergebenen Parameter an die formalen Parameter
zugewiesen.
- Vorgabeparameter, z.B. myFunc(par1, par2 = 17);
Als Vorgabeparameter kann ein literaler Ausdruck stehen.
Vorgabeparameter können - zur Dokumentation - auch bei vorwärtsdeklarationen
verwendet werden, sind dort aber bedeutungslos.
Hat eine Funktion Vorgabeparameter, so wird sie implizit immer mit mindestans allen
definierten Argumenten aufgerufen (mehr sind möglich).
Deklaration von Klassen
- BOB+2 gestattet Vorwärtsdeklarationen von Klassen im C++-Stil, z.B.
class MyClass;
- Bei Klassendeklarationen ist hinter der schließenden Klammer (}) ein optionales
Semikolon zulässig.
Es sollte dann notiert werden, wenn eine Referenz mit Doxygen erzeugt werden soll
- sonst erkennt Doxygen das Ende der Deklaration nicht.
- Auch Methoden von Klassen können Vorgabeparameter haben
- Zur Vermeidung undefinierter Zustände ist der Compiler strenger:
- Ist eine Klasse von einer Basisklasse abgeleitet, so kann sie bei einer Neudefinition
(Redefinition, Erweiterung) nicht von einer anderen Basisklasse abgeleitet werden.
- Einer Klasse, die von einer anderen Klasse als Basisklasse verwendet wird, können keine zusätzlichen nicht-statischen Member-Variablen
hinzugefügt werden.
- Bereits definierte Member-Variablen können nicht nachträglich als static umdefiniert
werden.
Überladen von Operatoren
- OP_CALL, OP_VREF, OP_VSET wie bisher
- neu - unäres Minus: OP_NEG
- neu - Inkrement,Dekrement: OP_INC, OP_DEC (pre/post-inkrement funktioniert für
alle Datentypen)
neue Operatorfunktionen OP_PINC und OP_PDEC für explizize Definition der post-Varianten
Wird für eine Klasse OP_INC bzw. OP_DEC definiert, ohne auch OP_PINC bzw. OP_PDEC
zu definieren,
so fügt der Compiler entsprechende Definitionen ein, die den Code von OP_INC/OP_DEC
mitbenutzen.
Achtung: umgekehrt gilt das nicht! Definiert man z.B. nur OP_PINC, so führt der
Präfix-Aufruf zu einem Fehler.
Gennerell ist es ratsam, stets beide Varianten zu definieren.
Die post-Varianten Operatorfunktionen sollten immer ein neues Objekt (anders als
in C++ das modifizierte, wobei das aktuelle unverändett bleibt!), die Prefix-Varianten dagegen eine Referenz auf das eigene
Objekt zurückliefern.
class A
{
A();
OP_INC();
OP_PINC();
OP_DEC();
OP_PDEC();
setI(val);
getI();
toString();
i;
}
A::A() { i = 0; }
A::setI(val) { i=val; }
A::getI() { return i; }
A::OP_INC()
{
++i;
return this;
}
A::OP_PINC()
{
var result = new A();
result->setI(i+1);
return result;
}
A::OP_DEC()
{
--i;
return this;
}
A::OP_PDEC()
{
var result = new A();
result->setI(i-1);
return result;
}
Achtung: Das zurückgegebene Objekt ERSETZT das ursprüngliche. Dies lässt sich vermeiden,
wenn man statt eines neuen Objekts das aktuelle - also this zurückgibt. Dann arbeitet
allerdings die Postfix-Variante nicht mehr korrekt, genauer: sie verhält sich wie
die Präfix-Variante.
- binäre Operatoren (+ - * / % & | ^ << >>) können für linken und
rechten Operanden überladen werden:
OP_ADD_R OP_ADD OP_SUB_R OP_SUB OP_MUL_R OP_MUL OP_DIV_R OP_DIV OP_REM_R OP_REM
OP_BAND_R OP_BAND OP_BOR_R OP_BOR OP_XOR_R OP_XOR OP_SHL_R OP_SHL OP_SHR_R OP_SHR
- neu - unäres bitweises NOT (~): OP_BNOT
- neu - compare-Funktion zum Überladen der Vergleichsoperatoren für Objekte
- neu - equals-Funktion zum Überladen der Operatoren == und !=. Sind sowohl equals
als auch compare
implementiert gewinnt equals. equals sollte 1 für Gleichheit und 0 für Ungleichheit
zurückgeben.
- neues Schlüsselwort operator zur syntaktischen
Vereinfachung der Operatordefinition:
opdef ::= [var] "operator" opspec "("argumentlist")".
opspec ::= (["L" | "R"] binop) | indexop | callop | unaryop.
binop ::= "+" | "-" | "*" | "/" | "%" | "&" | "|" | "^" | "<<" | ">>".
indexop ::= "[" ["var"] "]".
callop := "(" ")" .
unaryop := "-" | "~" | "++" | "--".
Ähnlich C++ werden Pre- und Postfix-Variante der Increment/Dekrement-Operatoren
durch die Argumentliste unterschieden. Für die Postfix-Varianten enthält die Liste
das Schlüsselwort "var", für die Prefix-Varianten bleibt sie leer.
Der unäre Minus-Operator wird vom binären dadurch unterschieden, dass bei ersterem
die Argumentliste leer ist.
Alle Operatordefinitionen werden vom Compiler in die korrespondierenden reservierten
Funktionsnamen umgesetzt.
Beim Indexoperator werden Get- und Set-Varianten dadurch unterschieden, dass bei
letzterem zwischen den eckigen Klammern das Schlüsselwort "var" notiert wird.
Das Präfix"R" kennzeichnet bei binären Operatoren die Variante für den rechtsseitigen
Operanden, "L" oder kein Präfix die für den linksseitigen.
Ausnahmebehandlung
- Prinzip und Syntax analog C++/JavaScript
- Da es keine strenge Typisierung gibt, gibt es zu jedem try nur einen catch-Block.
- Fest implementierte ("native") Funktionen führen bei Fehlern nicht mehr zwangsweise
zum Programmabbruch sondern lösen eine Ausnahme vom String-Typ aus, die im Code
behandelt werden kann.
Beispiel:
try
{
myVar = anyFunc();
if (myVar) < 0)
throw "myVar must not be negative";
// do something else
}
catch (e)
{
print(e, "catched\n");
throw; // rethrow exception
}
Fallunterscheidung (switch-case)
- syntaktisch wie C/C++, Java(Script) u.ä.
- Anders als in C++ kann hinter case
nicht nur eine Konstante sondern ein beliebiger Ausdruck stehen. Der Ausdruck hinter
switch kann ebenfalls beliebigen Typs
sein, sofern ein Vergleich mit den Werten hinter
case definiert ist.
- Wie in C/C++ gibt es ein "fall through", d.h. wird ein
case-Zweig nicht mit break
verlassen, werden die Anweisungen des folgenden Zweiges mit ausgeführt.
Beispiel:
switch (myVariable)
{
case 1:
// do something
break;
case 2:
// do any other
// fall through
default:
// do something else
}
Neue vordefinierte globale Variablen
Die Variablen (die eigentlich als Konstanten zu verstehen sind) sollen helfen, plattformunabhängige
Programme zu schreiben. Anders als die vordefiniertten Literale bekommen sie ihren
Wert nicht zur Kompilier- sondern zur Laufzeit.
- __OSTYPE__ für den Typ des Betriebssystems, mögliche Werte "WIN32", "WINCE", "DOS"
oder eine leere Zeichenkette für andere Plattformen (tritt praktisch nicht auf)
- __UNICODE__ (long) mit Wert 1, wenn das Programm auf einem UNICODE-Build läuft,
sonst 0.
Vordefinierte Funktionen
- newbuffer, newcbuffer, newdict zum Erzeugen von Binär- und Zeichenpuffern
sowie Dictionarys
- newstring kennt drei Aufrufvarianten:
- newstring(string|buffer|charbuffer) ... neuer String aus einfachem Wert
- newstring(int size, fillchar=' ') ... string mit Anfangsgröße und Initialisierung
- newstring(formatStr, ...) ... String aus Formatangabe und Werteliste (analog sprintf)
- size - liefert Anzahl der Elemente eines Containers (Binär- oder Zeichenpuffer,
Vektor, Dictionaey)
- cb = CBuf(...) // argtypes: int, buffer, charbuffer, string; other ignored
... konstruiert einen Zeichenpuffer aus einer Argumentliste
- dictcontains, dicterase, dictclear,dictgetkeys, dictgetvalues ... Dictionary-Funktionen
- buffer(var) ... Typumwandlung in Buffer
- LoadLibrary, FreeLibrary, GetProcAddress, CallLibFunc ... DLL-Funktionen ;
in Windows-Builds wird ? im Funktionsnamen bei GetProcAddress durch A (Ansi) bzw.
W (Unicode) ersetzt, unter WinCE darf die aufgerufene DLL-Funktion max. 16 Argumente
haben
- In allen Windows-Builds neue Funktionen GetUser32Func, GetKernel32Func, GetShell32Func,
GetGdi32Func und GetOle32Func. Alle diese Funktionen liefern einen Zeiger auf eine
Funktion aus der entsprechenden DLL (ähnlich GetProcAddress), wobei aber Laden und
Verwalten der DLLs intern erfolgt. Außerdem gibt es einen Alias GetCoreFunc, der
GerUser32Func entspricht. Unter WindowsCE bezoehen sich alle Aufrufe an GetUser32Func,
GetKernel32Func, GetShell32Func und GetGdi32Func auf die coredll-Bibliothek (daher
auch der Alias-Name).
- Ebenfalls in Windows-Builds: neue Funktionen GetLastError (Wrapper für die gleichnamige
Windows-Funktion) sowie GetSysErrorMessage([errcode]) zum Holen der Text-Fehlermeldung
zu einem Code. Fehlt errcode, so wird implitit GetLastError aufgerufen.
- neue Systemfunktionen getenv(string name) und setenv(string name, string value);
unter WinCE wird Environment in HKCU\Environment nachgebildet
- neue I/O-Funktionen
- long fread(buffer,[[unsigned size=1,]unsigned cnt,]FILE* fp) ... aus Datei in
Puffer lesen
- long fwrite(buffer,[[unsigned size=1,]unsigned cnt,]FILE* fp) ... aus Puffer in
Datei schreiben
- in Windows-Builds: getwc, putwc, fgetws, fputws ... Unicode-File-I/O
- neue String-Funktionen (* .. Quellstring str wird modifiziert)
- strfind(str,searchstr) ... suchen, liefert Position oder -1
- substr(str,startpos,length=-1) .. Teilstring bilden
- strreplace (str,search,replace) ... Teilstring oder Zeichen ersetzen (*)
- strerase(str,startpos,cnt=-1) ... Teilstring löschen (*)
- strinsert(str,pos,insertstr) oder strinsert(str,pos,insertchar,cnt=1) .. Einfügen
(*)
- Die Typumwandlungsfunktion string hat (wie in Version 1.0) nur ein Argument und
akzeptiert die Typen int, double, float, string ,cbuffer bzw.object. Objekte müssen
zur Umwandlung in eine Zeichenkette eine Methode toString implementieren.
Aufbau des Bytecode-Vektors einer Funktion
Index | Symbol | Beschreibung |
0 | IDX_CODE | Bytecode-Puffer (BppBuffer) |
1 |
IDX_CLASS |
Verweis auf Klasse (bei Mehoden), sonst null |
2 |
IDX_NAME |
Funktionsname
|
2 |
IDX_FIRSTLITERAL |
erstes Literal (=Funktionsname) |
3 |
IDX_DEFAULTARGS |
Vektor mit default-Argumentwerten |
4 |
IDX_SELFREFERENCE |
Zeige auf eigenen Code-Vektor (self-Referenz) |
... |
|
Werte von Literalen und lokalen statischen Variablen |
(last) |
|
Vector(Vector(paramnames),Vector(localvarnames)) - nur wenn _genLocalVarInfo in
Compiler gesetzt; wenn statische lokale Variablen existieren, enthält localvarnames
ein zusätzliches Element (letztes, Dictionary) mit Zuordnungen der Namen der lokalen
statischen Variablen zu Indizes im Vektor. |
Stack-Frame bei Funktionsaufruf
Index |
Beschreibung |
BP |
Objektverweis bei Methodenaufrufen |
BP+1 |
1. Argument |
BP+nargs |
letztes Argument) |
FP (= BP+nargs) |
vor erster lokalen Variablen (falls vorhanden) |
FP+1 |
erste lokale Variable (reserviert für throw Exception) |
... |
lokale Variablen (mit erstem opcode eingefügt) |
SP |
aktueller Stack-Pointer |