Code-Completion für die Sprache Boo in SharpDevelop
BooBinding ist mein Projekt für Jugend forscht 2006.
Auf dieser Seite finden Sie Informationen über meine Teilnahme bei Jugend forscht, nach dem Wettbewerb stelle ich hier auch
die Schriftliche Ausarbeitung zur Verfügung.
BooBinding ist in SharpDevelop 2.0 enthalten und kann hier heruntergeladen werden: sharpdevelop.net
Über SharpDevelop
SharpDevelop ist eine Open Source IDE, mit der Anwendungen in den Programmiersprachen C# und Visual Basic geschrieben werden können.
Das Projekt wurde 2000 gestartet von Mike Krüger, viele andere Personen haben seitdem Code beigetragen. Seit dem Sommer 2004 arbeite auch ich an SharpDevelop mit. Mike Krüger hat das Projekt Anfang 2005 verlassen, seitdem bin ich der Hauptentwickler. Für SharpDevelop 2.0 habe ich die Code-Completion-Architektur umgeschrieben, um die neuen Sprachfeatures aus .NET 2.0 zu unterstützen und gleichzeitig die Integration neuer Programmiersprachen in die Entwicklungsumgebung zu vereinfachen.
Gleichzeitig habe ich SharpDevelop so angepasst, dass es nicht nur Code-Vervollständigung beim Drücken von . bietet, sondern auch weitergehende Funktionen wie Gehe zu Definition, Finde Referenzen und intelligente Tooltips anbietet.
Abbildung 1: SharpDevelop 2.0
Über Boo
Ich habe die Sprache Boo Ende 2004 kennen gelernt. Boo hat das Ziel, sowohl leicht lesbar als auch Handgelenk-freundlich zu sein [Oliveira, S. 2]. Damit ist gemeint, dass Boo Konstrukte bereitstellt, die leicht einzugeben sind und keine überflüssigen Informationen eingetippt werden müssen, aber der Programmcode trotzdem leicht zu lesen und zu verstehen ist.
Die Sprache Boo basiert, genau wie C#, auf dem .NET Framework. Die Syntax von Boo hingegen ist an Python angelehnt. Boo ist wie C# statisch typisiert allerdings zwingt der Compiler den Programmierer nicht, ihm Dinge einzugeben, die er eigentlich schon wissen muss [Oliveira, S. 2].
def one():
return 1
um = one()
In diesem Beispielprogramm gibt one klar einen Wert vom Typ int zurück, und die Variable um hat auch den Typ int. Die Sprache Boo ist in der Lage, die Typen beim Kompilieren selbst zu erschließen. Dies ist ein großer Unterschied zu Python und vielen anderen Skriptsprachen, wo Variablen keinen festen Typ haben, sondern erst zur Laufzeit dynamisch einen Typ zugewiesen bekommen.
Abbildung 2: Code-Completion
So zeigt SharpDevelop mit meiner Boo-Erweiterung beim Drücken von . eine Liste mit den Methoden an, die auf dem Ausdruck ausgeführt werden können. (im Beispiel die Methoden auf System.String)
Damit der Programmierer nicht beim Tippen gestört wird, muss diese Liste schnell erscheinen den Boo Compiler zu starten, um den Typ von text zu ermitteln, kommt hier nicht in Frage.
Ein weiteres Feature, das ich in SharpDevelop implementiert habe, ist Gehe zu Definition.
Drückt der Benutzer Strg+Enter, springt der Cursor zur Definition des aktuellen Elements. Ist der Cursor auf text platziert, wird zur ersten Zuweisung an text gesprungen; ist der Cursor auf greet() platziert, so wird zur Definition
Daher ist es möglich, dem Programmierer beim Schreiben des Codes Hilfen anzubieten, denn die Entwicklungsumgebung kann, wie der Compiler, den Typen einer Variablen ermitteln.der Methode gesprungen auch, wenn diese in einer anderen Datei liegt.
Parser-Theorie
Mike Krüger und Andrea Paatz haben für SharpDevelop bereits einen C#-Parser geschrieben. Es ist ein LL(k)-Parser basierend auf Coco/R, einem Programm, das aus einer Grammatik-Definitionssprache ein Parserprogramm erstellt. Solche Programme nennt man Parser-Generatoren oder auch Compiler-Compiler, obwohl die nur den Parser und nicht den Rest des Compilers erstellen. Für Boo hingegen benutzte ich den Parser aus dem Boo Compiler, geschrieben von Rodrigo B. de Oliveira.
Ein Parser funktioniert folgendermaßen:
Der Programmtext wird durch den
Lexer in so genannte Token zerlegt. Aus gradient as Brush = LinearGradientBrush(
wird somit: ID: gradient, As, ID: Brush, Equals, ID: LinearGradientBrush, OpenParenthesis
(ID steht für Identifier). [Vergleiche Holm, S. 292]
Das bedeutet, der Lexer fasst den Programmtext in Wort- und Zeicheneinheiten zusammen, dabei werden Schlüsselwörter identifiziert. Kommentare und überflüssige Leerzeichen werden entfernt, wobei die Kommentare für spätere Verwendung in einer getrennten Liste gespeichert werden.
Der Parser generiert aus diesen Token den Abstract Syntax Tree. Dies ist eine Sammlung von Objekten, die den Programmcode wiedergibt und die Struktur enthält.
Die Zeile a = b + c; ist im gesamten ein ExpressionStatement, also eine Anweisung, die einen Ausdruck ausführt. Der Ausdruck a = b + c ist vom Typ AssignmentExpression, mit einer IdentifierExpression auf der linken Seite und einer BinaryOperatorExpression auf der rechten Seite.
Der Abstract Syntax Tree ist also eine Baum-Struktur, die den Programmcode wiedergibt.
Sämtliche Parser (C#, VB und Boo) wurden getrennt voneinander entwickelt. Jeder besitzt daher seinen eigenen Abstract Syntax Tree (AST) mit leicht unterschiedlichen Konstrukten. Zudem kommen in der Entwicklungsumgebung auch noch Informationen aus eingebundenen Bibliotheken zusammen.
Daher existiert zusätzlich zu den (sprachabhängigen) Parserklassen ein sprachunabhängiges Typsystem, genannt ICSharpCode.SharpDevelop.Dom.
Das Typsystem
Im Gegensatz zu den Syntax Trees der Parser enthält das Typsystem nur Informationen über vorhandene Klassen und Methoden, nicht aber den Körper der Methoden.
Daher kommt das Typsystem mit wenigen Klassen aus.
Abbildung 3: Klassen im Typsystem
Ein ProjectContent entspricht einem Projekt oder einer eingebundenen Bibliothek und kann andere Projekte oder Bibliotheken referenzieren. Ein Projekt enthält pro kompilierbarer Datei eine CompilationUnit, bei Bibliotheken hingegen wird nur eine einzige CompilationUnit für die ganze Bibliothek verwendet.
Ein Using bezieht sich auf ein Import-Statement im Code, durch das Klassen aus anderen Namespaces eingebunden werden. Dies ist wichtig, um von den kurzen Typnamen (z.B. Regex) auf den vollständigen Typnamen (z.B. System.Text.RegularExpressions.Regex) zu schließen.
Jede CompilationUnit kann beliebig viele Klassen enthalten. Jede Klasse wiederum enthält Methoden (z.B. ToString()), Eigenschaften (z.B. Text), Felder (z.B. text) und Ereignisse (z.B. TextChanged).
Sowohl für Rückgabetyp von allen Klassenmitgliedern als auch für Methodenparameter und für die Basisklassen, von denen eine Klasse erbt, werden Verweise auf andere Typen benötigt.
Wichtig ist hier die Unterscheidung von Typen und Klassen für Methodenparameter können nicht nur andere Klassen, sondern auch Konstrukte wie Arrays, konstruierte Typen (List<string>) als Parameter oder Rückgabewerte verwendet werden.
Außerdem ist der vollständige Typname beim Erstellen des Typsystems nicht unbedingt bekannt wird Sourcecode geparst, benutzt er möglicherweise Klassen aus Dateien, die erst später geparst werden. Dies war ein Problem in SharpDevelop 1.1, dass ich mit diesen Klassen für Typreferenzen gelöst habe:
Abbildung 4: Klassen, die Typreferenzen repräsentieren
Das Interface IReturnType definiert Methoden, mit denen die Methoden (und Eigenschaften, Felder und Ereignisse) auf einem IReturnType zurückgegeben werden. Sämtliche Klassen, die von ProxyReturnType erben, geben die Liste von einem Basis-Returntype zurück. Im Fall von ConstructedReturnType wird die Liste manipuliert, um die Typargumente einzufügen.
Als Beispiel betrachten wir die Reference List<string> (C#).
Die gesamte Referenz ist vom Typ ConstructedReturnType. Der Basistyp ist ein SearchClassReturnType, denn List wurde nicht als vollständiger Typname angegeben. Das Typargument <string> ist ein Schlüsselwort der Sprache C# und wird daher direkt als DefaultReturnType eingebunden.
Wird nun die Liste der Methoden des ConstructedReturnType abgerufen, so ruft dieser zunächst die Methoden des zugrunde liegenden SearchClassReturnType ab. Der SearchClassReturnType sucht mit Hilfe der Usings aus der CompilationUnit, aus der er erzeugt wurde, nach seinem zugrunde liegenden Typ, der wiederum als Basistyp verwendet wird. Dies ist in der Regel ein DefaultReturnType, der direkt eine Klasse referenziert. Ein DefaultReturnType ist direkt mit einer Klasse aus dem Typsystem verbunden. Er gibt aber nicht nur die Methoden der Klasse zurück, sondern auch die Methoden, die die Klasse von ihren Basisklassen vererbt bekommt.
Diese Liste der Methoden wird zurückgegeben bis zum ConstructedReturnType. Dieser sucht nun nach Verweisen auf GenericReturnTypes. Im Fall von List<string> findet der ConstructedReturnType die Methode Add(T item) und ersetzt in einer Kopie den Typparameter, so dass Add(string item) zurückgegeben wird.
Das Typsystem repräsentiert so den verfügbaren Code zum Abrufen der Code-Completion-Informationen.
Jedoch ist eine Konvertierung von Parser AST zu ICSharpCode.SharpDevelop.Dom notwendig.
Das Visitor-Pattern
Der vollständige AST der Sprache Boo ist sehr komplex es sind 80 verschiedene Klassen, die jeweils unterschiedliche Sprachelemente repräsentieren. Dabei werden einige Klassen mehrfach verwendet, z.B. BinaryExpression für alle Operatoren mit zwei Operanden (es gibt 37 Stück), und CastExpression für alle Arten von Typumwandlung: Konvertierung, Cast und TryCast (in C# mit dem Schlüsselwort as).
Es ist nicht sinnvoll, eine ConvertToSharpDevelopTypeSystem-Methode in jede dieser Klassen einzufügen. Es sollte möglich sein, ohne Änderung des Boo-Kompiler-Codes auszukommen. Alle 80 Klassen durch Vererbung zu spezialisieren kommt auch nicht in Frage.
Der Konverter muss jedoch an vielen Stellen eine Unterscheidung je nach Typ eines Ausdrucks machen, da jedes Sprachelement eigenen Code für die Ausgabe benötigt. Die auszuführende Ausgabemethode hängt also von zwei Kriterien ab: dem Typ des Konverters und dem Typ des zu konvertierenden Ausdrucks. Die Sprachen C# und Boo unterstützen mit virtuellen Methoden jedoch nur den Aufruf in Abhängigkeit von einem Typ.
Hier bietet das Visitor-Pattern eine elegante Lösung. Eine Schnittstelle IAstVisitor definiert Methoden für alle möglichen Sprachelemente:
object Visit(ForStatement forStatement, object data);
object Visit(LabelStatement labelStatement, object data);
object Visit(GotoStatement gotoStatement, object data);
object Visit(SwitchStatement switchStatement, object data);
Jede einzelne der Sprachelementklassen besitzt die Methode
public override object AcceptVisitor(IASTVisitor visitor, object data)
{
return visitor.Visit(this, data);
}
Der Konverter ist eine Klasse, die IAstVisitor implementiert. Durch den Aufruf von node.AcceptVisitor(this, data); kann der Konverter die passende Methode für das aktuelle Sprachelement aufrufen. Somit muss die Fallunterscheidung nicht von Hand programmiert werden, sondern wird durch den Methodenaufruf automatisch durchgeführt.
Die Klasse, die die Visit-Methoden enthält und somit eine Operation auf der Klassenhierarchie darstellt, wird Visitor genannt, die einzelnen Elemente des AST sind Nodes.
[Vergleiche Gamma, S. 338]
Einbinden des Boo Parsers in SharpDevelop
Der Boo Parser wird über den SharpDevelop AddIn-Tree eingebunden. Die genaue Funktionsweise des AddIn-Trees kann hier nicht erklärt werden (siehe dazu meinen Code-Projekt Artikel: http://www.codeproject.com/useritems/ICSharpCodeCore.asp)
Im Prinzip wird für die Dateiendung .boo eine Parserklasse registriert, die ein IParser-Interface aus SharpDevelop implementiert. Diese Parserklasse ruft einige Arbeitsschritte aus dem Boo-Compiler auf als wichtigstes den Boo-Parser, zudem werden auch noch Funktionen aus dem Boo-Compiler genutzt, um Makros (ein Sprachfeature von Boo) anzuwenden.
Der zurückgegebene Syntax Tree wird durch Anwendung des Visitor-Patterns in Objekte des SharpDevelop Typsystems umgewandelt.
SharpDevelop übernimmt dann die Darstellung, zum Beispiel in der Klassenansicht:
Abbildung 5: Klassenansicht
Jetzt befinden sich die Klassen aus dem Boo-Code und die Klassen aus den benutzten Bibliotheken im gemeinsamen Typsystem, zur Code-Vervollständigung fehlen allerdings noch entscheidende Elemente.
ExpressionFinder
Beim Drücken von . oder aber auch beim Anzeigen des Tooltips muss die Entwicklungsumgebung zunächst wissen, von welchem Ausdruck sie den Typ ermitteln muss. Da die Syntax mit ihren Kommentaren und Begrenzungen von Strings und anderen Literalen sprachabhängig ist, definiert SharpDevelop dazu eine Schnittstelle IExpressionFinder. Ein ExpressionFinder muss aus einem Dokument den Ausdruck vor oder an einer Position ermitteln können. In dem Programmcode SomeMethod(filename.Substring(filename.IndexOf(')'))) wird die Maus über Substring gehalten. Es sollte ein Tooltip mit der Beschreibung von string.Substring(int) angezeigt werden. Das Beispiel habe ich gewählt, weil ich daran einige Dinge demonstrieren kann. Die Suche nach einer Klammer in einem Dateinamen ist zwar nicht sinnvoll, es zeigt aber, dass der ExpressionFinder die Klammer im String von den Klammern im Code unterscheiden kann.
Dazu sind drei Schritte notwendig:
- Ermitteln des Ausdrucks, auf den der Mauszeiger zeigt
- Ermitteln des Typs von filename
- Ermitteln des passenden Overloads der Methode Substring
Zusammen mit etwas Zusatzcode
vereinfacht dies den Programmcode soweit, dass Kommentare ignoriert werden,
String-Literale durch einen leeren String und Regex-Literale durch einen
leeren regulären Ausdruck ersetzt werden. Dann braucht der Rest des
ExpressionFinder
Der ExpressionFinder ist für die erste Aufgabe, das Ermitteln des
Ausdrucks, gedacht. Ich habe ihn als Zustandsmaschine implementiert, die
zunächst den Programmcode vereinfacht.
Abbildung 6: Die Zustandstabelle des ExpressionFinder
lediglich die Klammern zu zählen, um Anfang und Ende des Ausdrucks zu finden.
Im Beispiel wird filename.Substring(filename.IndexOf(')')) zurückgegeben. Zudem gibt der ExpressionFinder noch den Kontext zurück, in dem der Ausdruck gefunden wurde. Momentan werden zusätzlich zum Standard-Kontext drei weitere Fälle betrachtet:
Steht der Ausdruck hinter dem Schlüsselwort as, so wird ein Typname erwartet. In Boo können, wie in C#, Eigenschaften denselben Namen wie ihr Typ besitzen:
public Color as Color:
Hier wird bei den beiden Wörtern Color ein unterschiedlicher Tooltip angezeigt. Der erste Color-Ausdruck hat den Standard-Kontext und bezieht sich damit auf die Eigenschaft, das zweite Vorkommen von Color hat den Typ-Kontext und bezieht sich damit auf die Klasse Color.
Eine ähnliche Unterscheidung existiert nach dem Schlüsselwort import, nach welchem ein Typname oder ein Namespace erwartet wird.
Der dritte Kontext schließlich ist der Attribut-Kontext: Boo erlaubt es, Meta-Daten als Attribute im Programmcode aufzunehmen. In der Deklaration [STAThread] def Main(): befindet sich der Ausdruck STAThread im Attribut-Kontext und bezieht sich auf die Klasse STAThreadAttribute.
Resolver
Beim zweiten Schritt, dem Ermitteln des Typs von einem Ausdruck, spielt die Klasse Resolver die wichtigste Rolle. Über eine Schnittstelle ist diese Methode definiert:
ResolveResult Resolve(ExpressionResult expressionResult,
int caretLineNumber, int caretColumn,
string fileName, string fileContent);
Der Parameter expressionResult beinhaltet das Ergebnis des ExpressionFinder (mit Kontext). C# und VB teilen sich einen Resolver, normalerweise hat aber jede Sprache hat einen eigenen Resolver. Der Resolver von Boo ermittelt zunächst von der angegebenen Cursorposition die aktuelle Klasse und Methode, die über das Typsystem bereitgestellt wurden.
Das gewünschte Ergebnis ResolveResult ist eine eigene Klasse, da weitergehende Informationen über das Ergebnis bereitgestellt werden müssen. In SharpDevelop 1.1 wurde lediglich der Rückgabetyp des Ausdrucks zurückgegeben. Dies war zwar ausreichend für Code-Vervollständigung; jedoch werden schon für Gehe zu Definition mehr Informationen über den Ausdruck benötigt. Daher habe ich eine Reihe von Klassen erstellt, die genauere Informationen über den Ausdruck angeben können.
Die möglichen Varianten des ResolveResult sehen folgendermaßen aus:
Abbildung 7: Die ResolveResult-Klassen
Jede ResolveResult-Klasse gibt den Rückgabetyp des Ausdrucks und die vorhandenen Zusatzinformationen an.
Unser Beispiel filename.Substring(filename.IndexOf(')')) wäre ein MemberResolveResult, da eine Methode aufgerufen wird. Der Rückgabetyp ist string, da Substring einen String zurückgibt. MemberResolveResult enthält aber auch eine Referenz auf die Substring-Methode im Typsystem und zwar auf die Methode, auf die die Parameter zutreffen.
Von Substring gibt es zwei Overloads, Substring(int startIndex) und Substring(int startIndex, int length). Der Resolver ist dafür verantwortlich, die Passende zu finden. Ist eine Methode einfach nur benannt, ohne aufgerufen zu werden, so ist dies nicht möglich. Für diesen Fall wird die Klasse MethodResolveResult benutzt.
Nun zur Arbeitsweise des Resolvers in meinem Boo-AddIn:
Nachdem die Typsystem-Objekte der aktuellen Methode und Klasse ermittelt wurden, wird der Boo Parser aufgerufen, um den AST für den angegeben Ausdruck zu erstellen. Der AST wird dann an einen speziellen Visitor, den ResolveVisitor, übergeben. Dieser ist für die eigentliche Arbeit beim Ermitteln des ResolveResult verantwortlich. Bei unserem Beispiel ist das äußerste AST-Element, das zuerst aufgerufen wird, eine MethodInvocationExpression.
Hier versucht der ResolveVisitor zunächst das Ziel des Methodenaufrufs (fileName.Substring) zu ermitteln. Dazu wird das Visitor-Pattern benutzt, um die entsprechende Methode des ResolveVisitor aufzurufen, OnMemberReferenceExpression.
Auch hier muss zunächst das Ziel des Ausdrucks (jetzt fileName) ermittelt werden. Über einen weiteren AcceptVisitor-Aufruf wird OnReferenceExpression aufgerufen.
Jetzt muss der ResolveVisitor das ResolveResult für fileName ermitteln. Lokale Variablen sind in Boo aber nicht einfach zu erkennen: die erste Zuweisung an eine nicht existierende Variable erzeugt implizit diese Variable. Deswegen sucht der Resolver in dieser Reihenfolge nach Elementen:
- explizite lokale Variablen
- Parameter der aktuellen Methode
- Member der aktuellen Klasse
- Namespaces und Klassennamen
- Member aus importierten Klassen und globalen Modulen
- implizite lokale Variablen
An dieser Stelle gehen wir davon aus, dass die Variable fileName explizit deklariert wurde.
Hierfür erstellt der Resolver ein LocalResolveResult, dass Position der Definition fileName as string und den Typ string als DefaultReturnType beinhaltet.
Dieses ResolveResult wird nun an die Methode OnMemberReferenceExpression zurückgegeben. Dort wird auf dem DefaultReturnType string u. a. GetMethods() ausgeführt und so die Methode Substring gefunden. Die MemberReferenceExpression string.Substring gibt also ein MethodResolveResult zurück, das auf die Klasse string und den Methodennamen Substring verweist. Dies wird an OnMethodInvocationExpression zurückgegeben. Hier analysiert der ResolveVisitor die Parameter an die Methode in diesem Fall filename.IndexOf(')'). Hierbei laufen OnMethodInvocationExpression, OnMemberReferenceExpression und OnReferenceExpression ähnlich wie bei Substring durch ergeben den Rückgabetyp System.Int32. Die genauen Regeln zum Ermitteln des Overloads werden im Abschnitt Overload resolution weiter erläutert. Das endgültige Ergebnis des Resolvers für unser Beispiel filename.Substring(filename.IndexOf(')')) ist somit ein MemberResolveResult, das auf string.Substring(int startIndex) verweist. Dies kann dann weiter von Code-Completion, den Tooltips oder Gehe zu Definition verwendet werden.
Suche nach Klassennamen
Im Beispiel war string ein Schlüsselwort und die dazugehörige Klasse war somit klar. Wenn jedoch der Benutzer einen Typ angibt, wird die Suche schwieriger.
Hier wird ein SearchClassReturnType benutzt. Dieser speichert den Kontext, indem der Typname gefunden wurde: Dateiname und Zeilennummer. Dies ist wichtig, da innere Klassen nur gefunden werden können, wenn die aktuelle Klasse bekannt ist. Der Dateiname bestimmt den aktuellen Namespace sowie das Set von Import-Statements, das für die Datei gilt.
Beim ersten Zugriff auf den SearchClassReturnType sucht dieser die seinen Basistyp, dessen Eigenschaften er über das Proxy-Pattern weitergibt.
Zuerst wird geprüft, ob der Name bereits vollständig ist und es eine Klasse diesen Namens gibt. Wenn ja, wird diese zurückgegeben. Ansonsten wird weitergesucht:
Zunächst wird der aktuelle Namespace durchsucht, danach die übergeordneten Namespaces. Ist die aktuelle Klasse im Namespace ICSharpCode.SharpDevelop.Test, so wird zunächst dort, dann in ICSharpCode.SharpDevelop und ICSharpCode gesucht.
Führt dies nicht zum Erfolg, so werden innere Klassen der aktuellen Klasse gesucht. Dabei wird nicht nur in der aktuellen Klasse, sondern auch in allen ihren Basisklassen gesucht.
Erst wenn dies alles nicht zum Ziel führt, werden die import-Statements benutzt. Diese suchen den Typ in den eingebundenen Namespaces oder aber prüfen, ob der Typname ein durch ein Import-Statement angegebener Alias ist, z.B. using StringPair = System.Collections.Generic.KeyValuePair<string, string>;
Type Inference
Boo unterstützt Type Inference das sind implizite Variablen, deren Typ durch den Typ des Initialisierungsausdrucks bestimmt wird, und Methoden, deren Rückgabetyp durch den Typ der return-Anweisung bestimmt wird. Ein Rückgabetyp einer Methode kann somit möglicherweise von anderen Methoden abhängen.
Beispiel:
Abbildung 8: Type Inference
Das erste Problem entsteht hier schon, wenn der Parser die Methode SucheDoppelpunkt im Typsystem eintragen soll: Es ist kein Rückgabetyp angegeben.
Zum diesem Zeitpunkt sind die Informationen über die Klasse string möglicherweise noch nicht verfügbar. Basistypen wie string werden zwar früh geladen, aber der Rückgabetyp könnte genauso gut von benutzerdefinierten Klassen, die der Parser erst später behandelt, abhängen.
Das Typsystem erwartet jedoch, dass direkt ein IReturnType der Methode zugeordnet wird.
Der Boo Compiler beherrscht zwar Type Inference, aber Aufrufe an den Boo Compiler würden hier erfordern, dass das gesamte Projekt kompiliert wird dies ist viel zu langsam für Tooltips oder Code-Vervollständigung in der Entwicklungsumgebung. Stattdessen muss Type Inference nur für die Elemente ausgeführt werden, die der Programmierer betrachten will. Deshalb musste ich für SharpDevelop Type Inference selbst implementieren, und zwar so, dass es auch funktioniert, wenn SucheDoppelpunkt vor string.IndexOf zum Typsystem hinzugefügt wurde.
Um dieses Problem zu lösen, erweitere ich das Typsystem im Boo-AddIn um eine weitere Klasse: InferredReturnType. So wie SearchClassReturnType werden auch hier Anfragen an GetMethods etc. an einen zugrunde liegenden Typ weitergereicht. Dieser Typ wird erst ermittelt, wenn nach der Liste der Methoden benötigt wird.
Der InferredReturnType speichert eine Referenz auf den Körper der Methode im Boo Abstract Source Tree. Wird nach der Liste der Methoden gefragt, so wird ein Visitor GetReturnTypeVisitor angewandt, um nach einem Return-Statement zu suchen. Der Ausdruck hinter dem Return-Statement wird dann an den Resolver gegeben, der dann nach dem vorhin beschriebenen Verfahren den Rückgabewert int entdeckt.
Die lokale Variable position hat ebenso einen InferredReturnType als Typ, der beim ersten Zugriff den Basistyp über den Resolver den Rückgabetyp der SucheDoppelpunkt-Methode findet und dieser Typ wiederum wird durch einen weiteren Lauf des Resolvers zu int aufgelöst (soweit der Rückgabewert der Methode nicht schon zur Darstellung in der Klassenansicht analysiert wurde).
Die Rückgabetypen lassen sich also beliebig schachteln und somit jedes Type Inference-Szenario wiedergeben. Hier ein komplexes Beispiel:
Abbildung 9: Type Inference mit Generatoren
Soll hier ein Tooltip für inhalt angezeigt werden, hat SharpDevelop schon mehr zu tun:
inhalt ist eine explizite lokale Variable. Ihr Typ wird ermittelt durch den Elementtyp der Auflistung inhalte, durch die iteriert wird.
Der Typ für inhalt wird somit zunächst dargestellt als [ElementReturnType: [InferredReturnType: inhalte]]. ElementReturnType ist eine weitere Klasse, die durch mein Boo-AddIn eingeführt wird. Der ElementReturnType gibt bei einem Aufruf von GetMethods() nicht die Methoden seines Basistyps (in diesem Fall InferredReturnType) zurück, sondern untersucht zunächst den Basistyp: ist es ein Array, werden die Methoden des Arrayelementtyps zurückgegeben, ist es eine Auflistung (System.Collections.Generic.IEnumerable<T>), so werden die Methoden des angegebenen Typarguments T zurückgegeben. Somit wird jeweils der Elementtyp der Auflistung zurückgegeben.
[InferredReturnType: inhalte] wird nun aufgelöst und ergibt [InferredReturnType: LeseTextDateiInhalte()]. Dies wird wieder aufgelöst und ergibt [InferredReturnType: File.ReadAllBytes(datei) for datei in dateien]. Dies ist ein Generator-Ausdruck (Boo AST: GeneratorExpression). Für jede Datei wird der Dateiinhalt zurückgegeben, d.h. zurückgegeben wird eine Auflistung von byte-Arrays. Der hier verwendete IReturnType ist [ConstructedReturnType: IEnumerable, Arguments={[InferredReturnType: File.ReadAllBytes(datei)]}]. Der Ausdruck des inneren InferredReturnType wird wieder aufgelöst und ergibt schließlich (wir ignorieren hier die Overload-Suche, für die datei auch noch über dateien aufgelöst werden müsste) den [ArrayReturnType elementType=[DefaultReturnType: System.Byte]].
ElementReturnType und ConstructedReturnType lösen sich praktisch gegenseitig auf, so dass dieser Rückgabetyp endgültig ist; er wird im Tooltip angezeigt.
Ein Problem entsteht bei so genannten Type Inference Cycles. Wenn zwei Methoden sich gegenseitig aufrufen, ist ihr Rückgabetyp (laut Boo Dokumentation) System.Object.
Mein bisheriger Code hingegen würde laufen, bis es zu einem Überlauf des Stacks kommt. Deshalb ist die Klasse InferredReturnType so implementiert, dass sie nur einmal den Resolver startet:
if (expression != null) {
Expression expr = expression;
expression = null;
cachedType = new BooResolver().GetTypeOfExpression(expr, context);
}
return cachedType;
Sollte beim Auflösen des Ausdrucks ein weiterer Aufruf an denselben InferredReturnType erfolgen, so wird der Standardwert für cachedType, System.Object, zurückgegeben.
Overload Resolution
Mit Overload Resolution ist gemeint, dass bei einem Aufruf an eine überladene Methode die passende Methode zum Aufruf gefunden wird. Dies ist wichtig für die Tooltips, Gehe zu Definition oder auch wenn die Methoden verschiedene Rückgabewerte haben.
Hier eine Auswahl einiger Überladungen von Console.WriteLine:
public static void WriteLine(int value);
public static void WriteLine(object value);
public static void WriteLine(string format, object arg0);
public static void WriteLine(string format, object arg0, object arg1);
public static void WriteLine(string format, object arg0, object arg1, object arg2);
public static void WriteLine(string format, params object[] arg);
Hier sind die Probleme beim Suchen eines Overloads zu kennen:
Eine einfache Prüfung der Argumentanzahl ist nicht möglich, da es Methoden mit dynamischer Argumentanzahl gibt.
Auch müssen die Regeln der Datentyp-Konvertierung der Entwicklungsumgebung bekannt sein ein Argument, das zu int konvertiert werden kann, würde den (int value)-Overload aufrufen, während andere benutzerdefinierte Datentypen den (object value)-Overload aufrufen würden.
Zudem gibt es generische Methoden und Type Inference für Typargumente. Diese Fälle werden auch von meinem Overload Lookup-Code behandelt.
Da Boo keine genaue Dokumentation zu den Regeln für Overload Resolution angibt, habe ich den Code für die C#-Regeln implementiert und somit für C# Code-Completion dieses bis dahin fehlende Feature hinzugefügt. Die Boo-Regeln scheinen nach meinen bisherigen Erfahrungen mit den Regeln für C# übereinzustimmen.
Die C#-Regeln sind in der C# Language Specification (ECMA-334), Kapitel 14.4.2 Overload resolution nachzulesen. Ich habe mich auf die dritte Version der Spezifikation bezogen, die C# 2.0 entspricht.
Dies wird komplizierter durch die Tatsache, dass bei unvollständigem Code SharpDevelop nicht wie der C# Compiler aufgeben sollte, sondern den Overload, den der Benutzer wahrscheinlich gemeint hat, zurückgeben sollte.

C#
zu Boo Konvertierung
Mein SharpDevelop-AddIn unterstützt auch die automatische Konvertierung von C#-Code in Boo-Code. Dazu wird der C#-AST in den von mir geschriebenen Konverter-Visitor eingegeben.
Der Konverter-Visitor könnte den Code direkt in der Sprache Boo ausgeben (als Textausgabe). Jedoch bietet sich hier auch die Verwendung des Boo-AST an, so dass mit den Boo-Objekten weitergerechnet werden kann. Des Weiteren bietet der Boo Compiler einen Ausgabe-Visitor, der aus dem Boo AST wieder Programmtext erstellen kann. Ich habe mich entschlossen, diesen Teil von Boo zu verwenden, damit der Konverter bei zukünftigen Änderungen in der Sprache Boo nicht angepasst werden muss, sondern einfach durch das Verwenden des neuen Ausgabe-Visitors der Programmcode mit der neuen Syntax erstellt werden kann.
Daher konvertiert mein Konverter-Visitor von einem Abstract Syntax Tree zum anderen.
Hier ein Beispiel aus dem Konverter:
public object Visit(ConditionalExpression cond, object data)
{
B.ConditionalExpression te = new B.ConditionalExpression();
te.Condition = ConvertExpression(cond.Condition);
te.TrueValue = ConvertExpression(cond.TrueExpression);
te.FalseValue = ConvertExpression(cond.FalseExpression);
return te;
}
B.Expression ConvertExpression(Expression expr)
{
return (B.Expression)expr.AcceptVisitor(this, null);
}
ConditionalExpression und Expression sind Klassen aus dem C#-AST, B.ConditionalExpression und B.Expression stammen aus dem Boo-AST. Da beide Sprachen sehr ähnliche Sprachelemente unterstützen, haben viele Klassen die gleichen oder zumindest ähnliche Namen. Daher ist der Boo-Namespace mit dem Alias B importiert.
Dieser relativ einfache Konverter-Code wiederholt sich für die meisten der 123 Sprachelemente aus dem C#-AST.
Anschließend werden besondere Anpassungen vorgenommen, um die Semantik des Programms beizubehalten, die ich hier allerdings nicht weiter ausführen kann, da diese Beschreibung nicht zu lang werden sollte. Zudem werden überflüssige Typinformationen wie m as MyClass = MyClass() entfernt und der Code dadurch vereinfacht: m = MyClass(). Schließlich wird der Ausgabe-Visitor des Boo Compilers dazu verwendet, um aus dem Syntax-Baum wieder Programmtext zu generieren.
Code-Generierung
SharpDevelop öffnet beim Druck der Tastenkombination Alt-Ins ein Menü mit Optionen zum automatischen Erstellen von Code basierend auf den Feldern und geerbten Klassen der aktuellen Klasse.
Abbildung 10: Alt-Ins Code-Generierung in SharpDevelop
In SharpDevelop 1.1 wurde fest C#-Code generiert, indem direkt der Code als String zusammengesetzt wurde. Dies machte es unmöglich, dieselben Funktionen für Boo zu verwenden. Daher habe ich den Code-Generator für SharpDevelop 2.0 umgeschrieben. Statt direkt Programmtext auszugeben, wird zunächst der Abstract Syntax Tree (mit den C#/VB-AST Klassen) generiert.
SharpDevelop AddIns können nun Code-Ausgabe für weitere Sprachen hinzufügen. Mein Boo-AddIn nutzt Teile meines Konverters, um den Code-Generator direkt Boo-Code ausgeben zu lassen. Somit funktionieren sämtliche Code-Generatoren, auch diejenigen, die erst in Zukunft entwickelt werden, mit meinem Boo-AddIn.
Forms Designer
Ein weiteres Feature meines Boo-AddIns möchte ich hier noch erwähnen: ich habe den SharpDevelop Forms Designer erweitert, so dass er Boo Programmcode laden und ausgeben kann.
Abbildung 11: Boo Forms Designer
Die Serialisierung und Deserialisierung der Komponenten im Designer ist vom .NET Framework vorgegeben. Sie benutzt System.CodeDom, ein weiterer Abstract Syntax Tree, der sprachübergreifend sein soll. CodeDom unterstützt aber nur Sprachfeatures, die allen .NET Sprachen gemeinsam sind. Außerdem repräsentiert CodeDom nicht direkt den Code, sondern ist bereits ein gebundener AST.
button1.Irgendetwas ist in Boo durch eine MemberReferenceExpression dargestellt. In CodeDom jedoch gibt es vier Klassen: CodeFieldReferenceExpression, CodePropertyReferenceExpression, CodeEventReferenceExpression und CodeMethodReferenceExpression. CodeDom ist nützlich zum Generieren von Code, da der Code-Generator so zusätzliche Informationen verfügbar hat und eventuell Spezialsyntax der Sprache benutzen kann. Die Konvertierung von CodeDom zu Boo ist einfach. Für die ASP.NET-Unterstützung von Boo wurde bereits ein solcher Konverter geschrieben, den ich hier wieder verwendet habe.
Die andere Richtung jedoch ist komplizierter: Der Resolver muss verwendet werden, um aus Boo-Code wieder CodeDom-Objekte zu erstellen, die dann vom Forms Designer geladen werden können. Diesen Konverter habe ich wieder als Visitor implementiert. Unterstützt werden längst nicht alle Konstrukte aus Boo, sondern nur Methodenaufrufe, Zuweisungen an Eigenschaften und Felder, Registrieren von Ereignis-Handlern und Erzeugen von Arrays.
Abbildungsverzeichnis
Abbildung 3: Klassen im Typsystem
Abbildung 4: Klassen, die Typreferenzen repräsentieren
Abbildung 6: Die Zustandstabelle des ExpressionFinder
Abbildung 7: Die ResolveResult-Klassen
Abbildung 9: Type Inference mit Generatoren
Abbildung 10: Alt-Ins Code-Generierung in SharpDevelop
Abbildung 11: Boo Forms Designer
Literaturverzeichnis
Holm, Christian: Dissecting a C# Application Inside SharpDevelop, Wrox, Birmingham, 2003
Gamma, Erich: Design Patterns, Addison-Wesley, 1998
de Oliveira, Rodrigo B.: Boo Manifesto http://boo.codehaus.org/BooManifesto.pdf
SharpDevelop im Internet: www.sharpdevelop.net