Die Microsoft .net Compiler Platform (Roslyn) - Teil 1: Grundlagen

Heute regnet es (Schnee). Ich bin mir nicht ganz sicher warum, aber den ganzen Tag muss ich an dieses Beispiel aus einer Vorlesung zum Thema Programmierung denken. Ihr wisst schon: Wenn es regnet und (...).
Gute Gelegenheit, mal einen Blick auf die Roslyn Specs zu werfen. Schon sehr cool, wenn man sich überlegt, was das Language Team da erschaffen hat. Aber was genau bedeutet das jetzt für mich als Entwickler? Und warum braucht Microsoft eigentlich einen neuen Compiler?

Die Motivation

Traditionell funktionieren und agieren Compiler wie eine "Black Box". Ich werfe meinen Programmcode hinein und es kommt Maschinencode wieder heraus. Programmcode erzeuge ich entweder mit VIM, Notepad++, Notepad oder einer Entwicklungsumgebung.
Aber was macht eine gute Entwicklungsumgebung aus? Zum einen, dass Sie mich beim Schreiben des Code unterstützt. Eine Programmiersprache besteht aus Keywords also Schlagwörtern hinter denen eine Funktionalität steckt. Dann kommen noch arithmetische und logische Operatoren dazu. Natürlich dürfen auch Ausdrücke, Kontrollstrukturen, Prozeduren und States nicht fehlen. Und alleine hinter dem Begriff States verbergen sich sehr viele Konstrukte – Variablen, Structures, Arrays, Klassen, um nur die bekanntesten Vertreter zu nennen. Eine gute Entwicklungsumgebung kennt diese Sprachfunktionalität und weisst mich, während ich schreibe, auf Fehler hin. Das nennt man dann Unterstützung zur Designtime – also die Zeit, in der ich das Programm schreibe.

Moderne Software besteht aus so vielen Einzelteilen und komplexen Zusammenhängen, sodass Entwicklungsumgebungen längst nicht nur für das Schreiben von Code verwendet werden. Der Entwickler von heute muss den Code auch verstehen können. Für das Lesen und Verstehen von Code bieten Entwicklungsumgebungen Funktionen wie Codeanalyse, Auffinden von Referenzen, Springen zu einer Definition und noch viele mehr. Diese Werkzeuge werden immer besser und die Projekte der Entwickler immer komplexer. Das Problem, vor dem Entwicklungswerkzeuge wie Visual Studio stehen, ist die Analyse des geschriebenen Codes.

JetBrains hat mit ReSharper ein statisches Code Analyse Tool entwickelt, das bei jeder Bewegung im eigentlichen Code Editor losläuft und beginnt das Geschriebene zu kompilieren und zu analysieren. ReSharper bietet so viele unglaublich hilfreiche Tools wie Refactoring, Convert to Expression, Check for Null und so weiter. Ich bin wirklich großer Fan von ReSharper und scheinbar nicht der einzige.
Aber Resharper bringt auch Probleme mit sich: Eines davon ist, dass es die Entwicklungsumgebung in großen Projekten so instabil macht, dass es keinen Spaß mehr macht, damit zu arbeiten. Ein weiteres Problem ist die statische Analyse selbst. Bei jedem Tastendruck muss bei der statischen Analyse ein Build-Prozess loslaufen. Hier kann man zwar optimieren, aber wirklich gut ist das Konzept nicht.
Fasst man die aktuelle Landschaft zusammen, kann man sagen, dass die Tools immer besser werden. Dafür stehen Sie aber auch vor immer größeren Herausforderungen. Und nicht nur die Tools werden komplexer, sondern auch die Projekte, die Entwickler heute umsetzen müssen. Genau hier setzt Roslyn an. Die Idee hinter Roslyn ist, dass die Entwicklungsumgebung den Code besser verstehen soll:

This is the core mission of the Roslyn Project: opening up the black boxes and allowing tools and end users to share in the wealth of information compilers have about our code.

Karen Ng, Matt Warren, Peter Golde, Anders Hejlsberg, The Roslyn Project September 2012

Wie funktioniert ein Compiler?

Ein Compiler arbeitet zumeist in zwei Phasen. Die erste ist die Analysephase, die für die syntaktische und lexikalische Analyse zuständig ist. In dieser wird auch der Code-Baum generiert. Die zweite Phase ist die sog. Synthesephase. Während dieser wird das Zielprogram erzeugt.

Analysephase

Lexikalische Analyse

Einer der wichtigsten Schritte während der Analysephase ist die lexikalische Analyse. Während dieser werden aus den Symbolen oder Zeichenfolgen "Lexikalische Tokens" oder auch (engl.) "Syntax Tokens" generiert. Syntax Tokens können sein:

  • Schlüsselworte (engl. keywords)
  • Bezeichner (engl. identifiers)
  • Zahlen (engl. literals)
  • Punkte (engl. punctuation)
  • ...

Syntax Nodes sind ein primäres Konstrukt, aus denen ein Syntax Tree besteht. Zu den Syntax Nodes zählt man Elemente wie:

  • Deklarationen
  • Statements ( Anweisungen )
  • Clauses ( Konstrukte wie if-else)
  • Expressions ( Ausdrücke )

Syntax Trivias sind Elemente, die größtenteils keinen Einfluss auf das Verständnis des Programmcodes haben. Zu den Trivias zählt man:

  • Whitespace ( Leerzeichen )
  • Comments ( Kommentare )
  • Preprocessor Directives
  • ...

Da diese Elemente nicht Bestandteil des Programmcodes sind und überall im Programmcode zwischen zwei "Syntax Tokens" erscheinen können, sind sie auch kein Bestandteil des klassischen "Syntax Trees" als Teil eines "Syntax Nodes". Denkt man allerdings an Szenarien wie Refactoring oder Clean-up sind Syntax Trivias ein wichtiges Element. Der Roslyn Compiler löst dieses Problem, indem er die Trivias dem entsprechenden "Syntax Token" als Leading Trivia voranstellt oder als Trailing Trivia nachlagert und so zur Verfügung stellt.

Ein weiteres wichtiges Element sind die Syntax Spans. Jedes Element wie Node, Token oder Trivia kennt seine Position innerhalb des Quellcodes und die Anzahl der Zeichen, aus denen es besteht. Die Textposition wird durch eine 32 Bit-Integer (Ganzzahl) repräsentiert. Dabei handelt es sich um einen Zero-Based Unicode Character Index.
Jedes Element hat zwei TextSpan-Eigenschaften: Span und FullSpan.
Bei Span handelt es sich um die Eigenschaft, die Auskunft über den Abstand des ersten Tokens im Node des Subtrees bis hin zum Ende des letzten Tokens gibt. Span berücksichtigt keine Trivias. Die FullSpan-Eigenschaft beinhaltet den Abstand der Tokens und gibt zusätzlich Aufschluss über den Abstand der Leading und Trailing Trivias.

Nach der allgemeinen Definition wollen wir uns das ganze einmal ansehen.

Syntaxtree

Die Aufgabe der Analysephase ist der Aufbau des "Syntax Trees". Schauen wir einmal auf eine ganz einfache Konsolenapplikation. Dazu erzeuge ich eine neue Konsolenapplikation. Um den Roslyn Syntax Visualizer verwenden zu können, benötige ich in der Visual Studio 2015 CTP 5 folgende Extension: Microsoft .net Compiler Platform Syntax Visualizer.

Nachdem das Plugin installiert ist, findet man unter dem Menüpunkt:
View - Other Windows - Roslyn Syntax Visualizer das CodeTree-Visualisierungswerkzeug.

Ich schreibe den Klassiker unter den „Hallo Welt“-Applikationen.

Was man hier sehr schön sehen kann, ist dass wir in unserem Syntax Tree ein "Syntax Node" haben, das "Using Directive" heißt. Das "Syntax Node" selbst besteht aus den beiden "Syntax Tokens": using und Semikolon. Zwischen den beiden Syntax Tokens befindet sich ein Syntax Node mit dem Namen Identifier Name mit einem Syntax Token, dem Identifier Token.
Warum ist der Identifier ein Syntax Node und nicht ein Syntax Token? Nun zum einen sind Bezeichner oder engl. Identifier sog. Syntax Nodes. Da Sie zumeist mit einer Deklaration und Bezeichnung im Code stehen. Denken wir mal an:

int i;

An diesem Beispiel können wir sehen, das Bezeichner in der Regel aus zwei Syntax Tokens bestehen. Zum einen aus der Typdefinition int (integer oder Ganzzahl) und zum anderen aus dem Bezeichner i. Und wir sehen die Trivias Whitespace zwischen using und system sowie das EndOfLine Trivia.

Kinds

Jedes Node, Token oder Trivia besitzt eine Kind-Eigenschaft. Diese gibt Auskunft über den entsprechenden Typ in Form des Enumerators "SyntaxKind". Jede Sprache also C# oder VB besitzt eine eindeutige "SyntaxKind"-Aufzählung (engl. Enumaration), die alle möglichen Nodes, Tokens und Trivia-Elemente in der Gramatik der jeweiligen Sprache abbildet. Das ganze erinnert an den Duden. Nur dass es sich hierbei um die Schlüsselworte der Sprache handelt. Aber auch eine Liste von Keywords trifft es wohl ziemlich gut.
Die Kind-Eigenschaft erlaubt es auf einem einfachen Weg, die Begrifflichkeit von "Syntax Nodes" zu unterscheiden, die sich die selbe Klasse von Nodes teilen. Einfach dadurch, dass man im Duden für C# nachschaut. Beispielsweise hat eine BinaryExpressionSyntax-Klasse drei Kind-Eigenschaften: Left, Right und Operator Token. Dank des Dudens für C# kann man via der Kind-Eigenschaft nun herauszufinden, um welche Kind-Eigenschaft es sich genau handelt.

AddExpression, SubtractExpression oder MiltiplyExpression sind mögliche Kinds des Syntax Nodes.

Farbschema

Um zwischen Syntax Tokens, Nodes und Trivias besser unterscheiden zu können, verwendet der Syntax Visualizer den folgenden Farbcode:

Da wir Syntax Node, Token und Trivia schon kennen, sollten wir auch einen Blick auf den letzten Farbcode werfen. Auch wenn es hart ist, sich mit pinken Dingen zu beschäftigen. Pink steht für Fehler oder Diagnostics.

Diagnostics oder Fehler

In der Spezifikation von Roslyn findet man folgenden Satz:

Even when the source text contains syntax errors, a full syntax tree that is round-trippable to the source is exposed.

Karen Ng, Matt Warren, Peter Golde, Anders Hejlsberg, The Roslyn Project September 2012

Das ist mal eine Ansage: Egal was ich für einen Blödsinn in die IDE eingebe, Roslyn wird versuchen einen Syntax-Tree daraus zu bauen, der iterriert werden kann. Roslyn verwendet zwei Techniken, um dieses Versprechen zu halten:

1. Missing Token Strategie

Wenn der Parser einen speziellen Typ von Token erwartet, dieser aber fehlt, kann Roslyn an die Stelle des erwarteten Tokens einen "Missing Token" einfügen. Ein "Missing Token" repräsentiert den aktuell erwarteten Token, hat aber einen Empty Span und die Eigenschaft (Property) IsMissing gibt den Wert "true" zurück. Das Ganze wird einfacher, wenn man es sich im Code Visualizer von Roslyn betrachtet.

Bei diesem Beispiel fehlt beim Methodenaufruf von WriteLine die schließende Klammer. Roslyn färbt nun die Nodes und Tokens entsprechend pink, für die eine Diagnostic besteht. Das ist insbesondere zur Lokalisierung von Fehlern für die Entwicklungsumgebung und Plugins wichtig. Damit können Tools wie Codemap den Fehler lokalisieren, auch wenn der Code gerade nicht sichtbar ist. Was man hier sehr schön sehen kann ist, wo genau der Fehler liegt:

  • Namespace: SimpleApp
    • Klasse: Program
      • Methode: Main
        • Ausdruck: Console.WriteLine

Hier finden wir einen OpenParenToken, der für "Open Parenthese" steht, also die öffnende Klammer, sowie die Argumente als "Syntax Node". Das "CloseParenToken", das für die schließende Klammer steht, ist zwar im Code nicht vorhanden, aber Roslyn erwartet an dieser Stelle eine schließende Klammer, weshalb hier ein CloseParenToken als MissingToken eingesetzt wurde. Und die IDE wird darüber informiert, dass an dieser Stelle eine schließende Klammer fehlt.

2. Skipped Token Strategie

Die zweite Strategie ist eine naive Herangehensweise: Wenn Roslyn mit gewissen Tokens so gar nichts anzufangen weiß, werden die Tokens vom Parser ignoriert, bis Roslyn wieder ein Token findet, das es parsen kann. Dieses Verhalten kommt mir sehr bekannt vor. Die unverständlichen Tokens werden als Trivia Tokens in den Syntax Tree gestellt und werden mit der Kind-Eigenschaft SkippedTokens versehen.

Das Beispiel zeigt, wie Roslyn versucht, das Geschriebene in einen korrekten Ausdruck zu verwandeln. Dabei wird ein Expression Statement mit einem OpenParenToken generiert und die Eigenschaft ContainsSkippedText auf True gesetzt. Interessant an diesem Beispiel ist, dass Roslyn das Node ExpressionStatement "Console.WriteLine" in Zeile 20 aber wieder als korrektes Node mit vollständigen Tokens parsen kann.
Jetzt da wir die Begrifflichkeiten kennen und eine grobe Vorstellung davon haben, wie SyntaxTrees funktionieren, können wir uns dieses Wissen zu nutze machen.

Roslyn APIs bringen Licht ins Dunkel

Roslyn selbst bietet eine Vielzahl von APIs an. Schauen wir nochmal kurz auf die Pipeline des Compilers.

Die Compiler APIs erlauben den Zugriff auf die einzelnen Phasen der Compiler Pipeline. Bisher haben wir über die Analysephase gesprochen und kamen zum Schluss, dass die Hauptaufgabe der Analysephase darin besteht, den SyntaxTree zu generieren. Für den Zugriff auf die Compiler APIs lege ich eine neue Konsolenapplikation an und installiere das Nuget Package - Microsoft.CodeAnalysis.

Wir benötigen die folgenden Namespace:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Da immer noch an den APIs gearbeitet wird, könnten diese sich allerdings noch ändern.

Im Namespace Microsoft.CodeAnalysis findet man den SyntaxTree, der das Parsen von Code erlaubt. Da wir in der finalen Version von Roslyn zwei Compiler haben werden, einen für C# und einen für VB, benötigen wir hier anders als bei der Version von 2011 einen passenden CodeType bzw. SyntaxTree, der über eine Factory generiert werden kann. Der fertige Code sieht dann also wie folgt aus:

SyntaxTree tree = CSharpSyntaxTree.ParseText("your code goes here");

Um auf den geparsten SyntaxTree zugreifen zu können, benötigen wir die Klasse CompilationUnitSyntax, die im Namespace Microsoft.CodeAnalysis.CSharp.Syntax liegt. Dank dieser Klasse können wir dann auf den C# SyntaxTree zugreifen und die einzelnen Nodes bzw. Tokens iterieren.

var root = (CompilationUnitSyntax)tree.GetRoot();

Man könnte zwar jetzt schon mit dem Debugger mehr darüber erfahren, aber wir wollen uns das Root-Element des Baumes erst einmal anschauen. Dazu erweitere ich unsere Applikation um eine weitere Zeile:

var firstMember = root.Members[0];

Hier setze ich nun einen Breakpoint und schaue mir die Datenstruktur genauer an:

Was wir bisher gemacht haben, ist die Syntaxanalyse, also den Aufbau des SyntaxTrees zu verfolgen. Ganz ähnlich wie die IDE dies macht. Eine komplette Anleitung zur syntaktischen Analyse findet man auf Github:

Sample Walkthrough Syntax Analysis

Hier wird beschrieben wie man den SyntaxTree analisieren kann. Das ist aber nicht das einzige was durch die APIs zur Verfügung gestellt wird.

Syntax Transformation

Natürlich kann die IDE nicht nur eine Syntaxanalyse durchführen, sondern auch die Änderungen am Code auf den Tree projezieren. Dies geschieht via Syntax-Transformationen. Das Stichwort lautet: "Erstellen und Modifizieren von Syntax Trees". Auch hierfür findet man auf Github ein entsprechendes Walkthrough:
Sample Walkthrough Syntax Transformation

Semantische Analyse

Um eine Semantische Analyse durchführen zu können, benötigt man eine sog. Compilation. Hat man erstmal eine Compilation, kann man Abfragen auf das semantische Model durchführen, zum Beispiel:

  • Welche Variablen sind im Scope?
  • Welche Members erlauben den Zugriff aus eine gewisse Methode?
  • Welche Variablen werden in einem bestimmten Block verwendet?
  • Welche Referenzen werden verwendet, und wo liegen Sie?
  • ...

Auf Github findet man auch ein entsprechendes Walkthrough zur semantische Analyse:
Sample Walkthrough Semantic Analysis

APIs für den Kompiler

Die Roslyn APIs erlauben aber noch weitere Szenarien wie z.B. die Durchführung eigener Codediagnosen, das unternehmensweite Einführen von Konventionen oder das Entwickeln von firmeneigenen oder Community-getriebenen Erweiterungen für Visual Studio. Alles in allem wird die dynamische Codeanalyse damit extrem vereinfacht.

Zusammenfassung

Dieser Beitrag ist der erste Teil einer Serie zum Thema Roslyn und speziell zum Thema C# (C-Sharp). Er sollte einen kurzen Überblick über Roslyn verschaffen. Mein Fazit: Die Vorteile, die Roslyn mit sich bringt, überwiegen die Nachteile. Der wahrscheinlich wichtigste Punkt ist das nun in kürzeren Zyklen neue Sprachfunktionalität hinzufügt werden kann. Was bisher drei Jahre dauern konnte, kann man nun in wenigen Wochen umsetzen. Der C# Compiler war bisher eine in C++ geschriebene Komponente, die unter dem Namen "csc.exe" bekannt war. Der neue C# Compiler ist in C# selbst geschrieben. Dank Performanceoptimierungen und cleveren Mechanismen ist die Kompilierungszeit nahe der vom alten C# Compiler. Man hat dafür aber eine moderne und stabile Codebasis. Das ganze Projekt ist außerdem Open Source und steht unter der Apache 2-Lizenz zur Verfügung.
Ein weiterer Vorteil ist die Entkopplung von Visual Studio. Die Visual Studio IDE und Ihre Features sowie Plugins und Extensions können nun Zugriff auf die Roslyn APIs nehmen, anstelle sich selbst einen logischen Reim auf gewisse Dinge zu machen. Das erleichtert das Umsetzen von neuen Projekten und neue Analysewerkzeugen, die gerade in großen Projekten und Teams benötigt werden.
Das Highlight ist aber die Lizenz und der Open Source-Ansatz. Es erlaubt eine Migration des C# Compilers auf andere Betriebssysteme und erleichtert Projekten und Firmen wie Mono und Xamarin das Leben extrem, da sie nicht mehr Reverse Engineering betreiben müssen, um die neuen C# Features zu integrieren.
In den nächsten Teilen dieser Blogserie gehe ich auf die neuen Sprachfeatures von C# sowie auf die neuen Möglichkeiten der Diagnostics API ein.