Compilerbau leicht gemacht

    • Allgemein

    Es gibt 11 Antworten in diesem Thema. Der letzte Beitrag () ist von Programmator.

      Compilerbau leicht gemacht

      Compilerbau leicht gemacht
      mit Visual Basic.Net
      Inhalt

      1. Die Einleitung
      2. Was wird benötigt
      3. Die Theorie
      4. Der Code
      5. Abschließende Worte


      1. Die Einleitung

      Ich heiße dich herzlich willkommen zu diesem Tutorial, in dem ich zeigen möchte dass es mit Hilfe von VB.Net sehr einfach ist einen eigenen Compiler zu bauen. Aber zunächst sollte man sich fragen: Für was brauche ich einen eigenen Compiler? Die Antwort ist aber schnell gefunden. Man kann einen eigenen Compiler dazu benutzen eine eigene Script- bzw. Programmiersprache zu kreieren. Diese kann man dann beispielsweise im eigenen Programm verwenden um damit Addons für das eigene Programm zu schreiben oder man versuchst es einfach mal aus Spaß.

      So und bevor wir hier weiter um den heißen Brei reden, würde ich sagen legen wir los, oder?

      2. Was wird benötigt

      Um genau zu sein braucht man nur eine IDE (z.B. Visual Basic .Net Express) und sein Hirn. Siehst du? Fängt doch schon einfach an!

      3. Die Theorie

      Ich weiß Theorie ist nicht immer schön, aber man soll ja auch das Prinzip verstehen und nicht einfach nur Abtippen.

      Hier möchte ich ein wenig auf die Funktionsweise eines Compilers eingehen. Zuerst einmal, was macht ein Compiler? Ein Compiler übersetzt den eingegebenen Code in eine andere Programmiersprache oder direkt in Maschinencode. Um den Code zu übersetzen teilt er diesen in sog. Token auf und verarbeitet diese dann einzeln weiter.
      Das kann man so verstehen:

      Der eingegebene Code:

      Quellcode

      1. Do Something Do Something else
      Dieser Code wird nun in Token zerkleinert:
      Token1 = Do Something
      Token2 = Do Something else

      Die Token werden in sich noch einmal mit dem selben Prinzip verarbeite. Tja mehr gibt’s hier eigentlich nicht zu sagen, ohne Leute die nicht mit der Materie vertraut sind zu verwirren. Und ehrlich gesagt ist das für uns hier auch nicht weiter von Bedeutung. Also weiter geht’s.


      4. Der Code

      Nun kommt der Interessante Teil. Wie wollen wir die Token Zehrlegung und die eigentliche Compelierungsarbeit umsetzen? Zu aller erst legt ihr eine neue Klasse an, Name ist egal.

      So nun brauchen wir ein paar Imports:

      VB.NET-Quellcode

      1. Imports System.Text
      2. Imports System.CodeDom.Compiler


      System.Text brauchen wir für den StringBuilder und System.CodeDom.Compiler ist ein dienst von Visual Basic mit dem man VB Code Compelieren kann, dass heißt der größte Teil wurde uns von Microsoft schon abgenommen ;)

      Jetzt sollten wir und mal überlegen wie unsere Klasse aufgebaut sein muss damit wir unser Ziel erreichen. Dabei kann man auf folgende Struktur kommen;

      VB.NET-Quellcode

      1. Public Class Compiler
      2. Public Function start()
      3. End Function Private
      4. Function parse()
      5. End Function Private
      6. Function CompileToken()
      7. End Function
      8. Private Function compile()
      9. End Function
      10. End Class

      Zu Erklärung:

      start() – Mit dieser Sub Routine startet man den Compiler
      parse() – Hier zerlegen wir den Code in die Token
      CompileToken() – Erklärt sich wohl selbst
      compile() – Hier schreiben wir den Fertigen Code in eine EXE


      Fangen wir dann mal mit start() an. Zuerst müssen wir uns überlegen welche Parameter wir brauchen. Zur Zeit brauchen wir nur den Code und den Namen des Programmes. Ansonsten rufen wir in start() nur die unterschiedlichen Routinen auf und geben zum Schluss noch einen Rückgabewert aus.
      Das sollte nun so aussehen:


      VB.NET-Quellcode

      1. Public Class Compiler
      2. Private linenr As Integer = 0
      3. Private endCode As StringBuilder
      4. Private ExeName As String = ""
      5. Public Function start(ByVal Code As String, Optional ByVal name As String = "App") As String
      6. Me.endCode = New StringBuilder("")
      7. Me.ExeName = name parse(Code.Split(vbCrLf).ToList)
      8. Return compile()
      9. End Function
      10. Private Function parse()
      11. End Function
      12. Private Function CompileToken()
      13. End Function
      14. Private Function compile()
      15. End Function
      16. End Class


      Die Variable endCode soll später den fertig Compelietren Code enthalten und wird dazu als StringBuilder festgelegt, weil dieser sich für so etwas eher anbietet als ein normaler String. Linenr gibt an in welcher Codezeile wir uns befindet, was uns später für die Fehler ausgabe zugute kommt.

      parse() ist nicht ganz so simpel. Zunächst mal müssen wir den Code in Token aufteilen.

      VB.NET-Quellcode

      1. Private Function parse(ByRef Tokens As List(Of String))
      2. If Tokens.Count = 0 Then Return False
      3. End Function


      Hier überprüfen wir auch gleich ob der Code überhaupt etwas beinhaltet.
      Nachdem man die Token erstellt hat muss man sie zunächst einmal „zuschneiden“ und kann man wie folgt machen:

      VB.NET-Quellcode

      1. Dim TokenStream As New IO.MemoryStream()
      2. Dim buffer() As Byte
      3. For Each ThisString As String In Tokens
      4. buffer = System.Text.Encoding.UTF8.GetBytes(ThisString.Trim(" "))
      5. TokenStream.Write(buffer, 0, buffer.Length)
      6. TokenStream.WriteByte(13)
      7. Next
      8. TokenStream.Position = 0


      Wir haben mit Visual Basic den Vorteil, dass Microsoft schon eine Artk kleinen Parser für uns Anbietet, den wir auch nutzen werden.

      VB.NET-Quellcode

      1. Dim SourceParser As New Microsoft.VisualBasic.FileIO.TextFieldParser(TokenStream)


      Damit wir aber mit dem TextFieldParser so richtig durchstarten können, brauchen wir noch ein paar Variablen.

      VB.NET-Quellcode

      1. Dim TokenFields() As String = {}
      2. SourceParser.Delimiters = {",", ", ", " "} 'Trennzeichen
      3. SourceParser.HasFieldsEnclosedInQuotes = True


      Die TokenFields werden später unsere nochmals unterteilten Token sein.
      So nun brauchen wir eine Schleife in der wir die einzelnen Token auslesen und sie weiter verarbeiten. Hier hilft uns der Befehel SourceParser.EndofData weiter:

      VB.NET-Quellcode

      1. While Not ScriptParser.EndOfData
      2. linenr = SourceParser.LineNumber
      3. Try
      4. TokenFields = SourceParser.ReadFields 'Versuchen den nächsten Token zu lesen
      5. Catch ex As Exception 'Wenn ein Fehler auftritt
      6. MsgBox("Error Message: " & ex.Message & vbNewLine, MsgBoxStyle.Critical, "Error") 'Fehler ausgeben
      7. TokenStream.Close() 'Tokenstream Schließen
      8. TokenStream = Nothing
      9. Exit While
      10. End Try
      11. If CompileToken(TokenFields) = False Then Return False 'Bei falschen Token, Compelierung beenden
      12. End While
      13. Return True


      Das wars dann auch schon mit parse(), aber sicher ist euch aufgefallen das wir in der Schleife CompileToken() aufrufen um den Token zu Compelieren und gegebenen Falls die weitere Compelierung stoppen.

      Bevor wir uns an CompileToken() ran wagen, sollten wir uns erst mal überlegen, welche Befehle unsere Sprache bekommen soll. In diesem Tutorial beschränke ich mich einfach mal auf die Nötigsten:

      PRINT – Gibt einen Text aus
      GET – Ließt eine Eingabe aus
      WAIT – Wartet auf eine Tasteneingabe

      VB.NET-Quellcode

      1. Private Function CompileToken(ByVal token() As String)
      2. Select Case token(0)
      3. Case "PRINT"
      4. Dim buf As String
      5. For i As Integer = 1 To token.Length - 1
      6. If (token(i).Substring(0, 1) = "$") Then
      7. buf += Chr(34) & " & " & token(i).Substring(1) & " & " & Chr(34)
      8. Else
      9. buf += token(i) & " "
      10. End If
      11. Next
      12. Me.endCode.Append("Console.WriteLine(" & Chr(34) & buf & Chr(34) & ")")
      13. Case "GET"
      14. Me.endCode.Append("Dim " & token(1) & " as String = Console.ReadLine()")
      15. Case "WAIT"
      16. Me.endCode.Append("Console.ReadKey()")
      17. Case Else
      18. MsgBox("Line: " & linenr & vbCrLf & "Reason: Unknown Command (" & token(0) & ")" & vbCrLf, MsgBoxStyle.Critical, "Error")
      19. Return False
      20. End Select
      21. Me.endCode.Append(vbCrLf)
      22. Return True
      23. End Function


      Puh! Ganz schön viel Code auf einmal, aber schnell erklärt. Wir überprüfen hier einfach den Token der uns übergeben wird und übersetzen ihn in VB Code. Im falle von PRINT brauchen wir noch eine schleife, denn wenn wir diese weglassen würden, würde bei „PRINT Hallo Welt“ nur das Hallo ausgegeben werden. Auch hab ich gleich eine überprüfung eingebaut ob sich im PRINT Befehl eine Variable befindet. Eine Variable wird mit $ erkannt. Ein Beispiel:

      Quellcode

      1. PRINT Die Variable ist: $var


      Damit können wir jetzt Theoretisch schon Programmieren.

      Quellcode

      1. PRINT Geb deinen Namen ein:
      2. GET name
      3. PRINT Dein Name ist: $name
      4. WAIT


      Der WAIT Befehl sollte zum Schluss immer da stehen, sonst schließt sich die Konsole später zu schnell und man kann ihren Inhalt nicht lesen.

      Die Funktion compile() geb ich euch jetzt einfach vor, denn es würde zu lange dauern euch den VBCodeProvider genau zu erklären. Dafür gibt’s schon genug Tutorials.
      Also hier die Letzte Funktion:

      VB.NET-Quellcode

      1. Private Function compile()
      2. Dim c As VBCodeProvider = New VBCodeProvider
      3. Dim icc As ICodeCompiler = c.CreateCompiler()
      4. Dim cp As CompilerParameters = New CompilerParameters
      5. cp.GenerateExecutable = True
      6. cp.GenerateInMemory = False
      7. Dim exeName As String = String.Format("{0}\{1}.exe", System.Environment.CurrentDirectory, Me.ExeName)
      8. cp.OutputAssembly = exeName
      9. Dim source As New StringBuilder()
      10. source.Append("Imports System" & vbCrLf)
      11. source.Append("Module Module1 " & vbCrLf)
      12. source.Append("Sub Main()" & vbCrLf)
      13. source.Append(Me.endCode.ToString & vbCrLf)
      14. source.Append("End Sub " & vbCrLf)
      15. source.Append("End Module " & vbCrLf)
      16. Dim cr As CompilerResults = icc.CompileAssemblyFromSource(cp, source.ToString())
      17. If cr.Errors.Count > 0 Then
      18. Dim er As String
      19. For Each err As CompilerError In cr.Errors()
      20. er += err.ToString() + vbCrLf
      21. Next
      22. Return er
      23. End If
      24. Return cr.PathToAssembly
      25. End Function


      In dieser Funktion wird euer bisheriger Code in eine Standard Konsolen Anwendung geschrieben und diese dann zu einem Programm Compeliert. Das fertig Compelierte Programm sollte dann im selben Ordner erscheinen wie euer Compiler (Bei einem Testdurchlauf wahrscheinlich in DEBUG Ordner).


      Zum Schluss noch einmal die gesamte Klasse:

      VB.NET-Quellcode

      1. Imports System.Text
      2. Imports System.CodeDom.Compiler
      3. Public Class Compiler
      4. Private linenr As Integer = 0
      5. Private endCode As StringBuilder
      6. Private ExeName As String = "App"
      7. Public Function start(ByVal Code As String, Optional ByVal name As String = "App") As String
      8. Me.endCode = New StringBuilder("")
      9. Me.ExeName = name
      10. parse(Code.Split(vbCrLf).ToList)
      11. Return compile()
      12. End Function
      13. Private Function parse(ByRef Tokens As List(Of String))
      14. If Tokens.Count = 0 Then Return False 'Token vorhanden
      15. Dim TokenStream As New IO.MemoryStream()
      16. Dim buffer() As Byte
      17. For Each ThisString As String In Tokens
      18. buffer = System.Text.Encoding.UTF8.GetBytes(ThisString.Trim(" "))
      19. TokenStream.Write(buffer, 0, buffer.Length)
      20. TokenStream.WriteByte(13)
      21. Next
      22. TokenStream.Position = 0
      23. Dim SourceParser As New Microsoft.VisualBasic.FileIO.TextFieldParser(TokenStream)
      24. Dim TokenFields() As String = {}
      25. SourceParser.Delimiters = {",", ", ", " "} 'Trennzeichen
      26. SourceParser.HasFieldsEnclosedInQuotes = True
      27. While Not SourceParser.EndOfData
      28. linenr = SourceParser.LineNumber
      29. Try
      30. TokenFields = SourceParser.ReadFields 'Versuchen den nächsten Token zu lesen
      31. Catch ex As Exception 'Wenn ein Fehler auftritt
      32. MsgBox("Error Message: " & ex.Message & vbNewLine, MsgBoxStyle.Critical, "Error") 'Fehler ausgeben
      33. TokenStream.Close() 'Tokenstream Schließen
      34. TokenStream = Nothing
      35. Exit While
      36. End Try
      37. If CompileToken(TokenFields) = False Then Return False 'Bei falschen Token Compelierung beenden
      38. End While
      39. Return True
      40. End Function
      41. Private Function CompileToken(ByVal token() As String)
      42. Select Case token(0)
      43. Case "PRINT"
      44. Dim buf As String
      45. For i As Integer = 1 To token.Length - 1
      46. If (token(i).Substring(0, 1) = "$") Then
      47. buf += Chr(34) & " & " & token(i).Substring(1) & " & " & Chr(34)
      48. Else
      49. buf += token(i) & " "
      50. End If
      51. Next
      52. Me.endCode.Append("Console.WriteLine(" & Chr(34) & buf & Chr(34) & ")")
      53. Case "GET"
      54. Me.endCode.Append("Dim " & token(1) & " as String = Console.ReadLine()")
      55. Case "WAIT"
      56. Me.endCode.Append("Console.ReadKey()")
      57. Case Else
      58. MsgBox("Line: " & linenr & vbCrLf & "Reason: Unknown Command (" & token(0) & ")" & vbCrLf, MsgBoxStyle.Critical, "Error")
      59. Return False
      60. End Select
      61. Me.endCode.Append(vbCrLf)
      62. Return True
      63. End Function
      64. Private Function compile()
      65. Dim c As VBCodeProvider = New VBCodeProvider
      66. Dim icc As ICodeCompiler = c.CreateCompiler()
      67. Dim cp As CompilerParameters = New CompilerParameters
      68. cp.GenerateExecutable = True
      69. cp.GenerateInMemory = False
      70. Dim exeName As String = String.Format("{0}\{1}.exe", System.Environment.CurrentDirectory, Me.ExeName)
      71. cp.OutputAssembly = exeName
      72. Dim source As New StringBuilder()
      73. source.Append("Imports System" & vbCrLf)
      74. source.Append("Module Module1 " & vbCrLf)
      75. source.Append("Sub Main()" & vbCrLf)
      76. source.Append(Me.endCode.ToString & vbCrLf)
      77. source.Append("End Sub " & vbCrLf)
      78. source.Append("End Module " & vbCrLf)
      79. Dim cr As CompilerResults = icc.CompileAssemblyFromSource(cp, source.ToString())
      80. If cr.Errors.Count > 0 Then
      81. Dim er As String
      82. For Each err As CompilerError In cr.Errors()
      83. er += err.ToString() + vbCrLf
      84. Next
      85. Return er
      86. End If
      87. Return cr.PathToAssembly
      88. End Function
      89. End Class


      5. Abschließende Worte
      Ich hoffe ich konnte euch einen guten Einblick in den Aufbau eines Compiler geben, muss aber sagen das wir hier nur den Simpelsten Compiler überhaupt gebaut haben, also nicht gleich überall damit Brüsten ;). Dann wünsche ich euch noch viel Spaß mit euren eigenen Compiler.
      Zum Schluss noch:

      Dieses Tutorial darf nicht ohne Erlaubnis des Autors verbreitet werden
      Dateien
      • Compiler.rar

        (12,55 kB, 482 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Madd Eye“ ()

      Da hat J-M schon Recht. Das hier ist nur ein Aufsatz, der die eigentliche Kompilierarbeit an einen richtigen Kompiler weiter reicht.

      Das ist so, als würde man sagen „ich werde jetzt Konditor“, erfindet neue Namen für Bestehendes (z.B. Schwarzwälderkirschtorte heißt jetzt Schnibbeltorte), und gibt dann den Auftrag mit dem richtigen Namen (also Schwarzwälderkirschtorte) an einen richtigen Bäcker weiter.

      Das ist normalerweise ein Hähne-Ei-Problem. In diesem Fall war die Hähne vor dem Ei da.
      Die Hähne? Der Hahn ist männlich und kann keine Eier legen, ist dann ja wohl klar, was da zuerst da war :P
      Ich denke du meinst die Henne^^
      Der Unterschied ist, dass du die Schnibbeltorte noch etwas anders aussehen lässt, aber die Zutaten dieselben sind, denn du ersetzt den Code ja durch VB-Code und kompilierst diesen...
      Ich wollte auch mal ne total überflüssige Signatur:
      ---Leer---
      Schon klar und ich schreib im Tutorial ja auch das dei Hauptarbeit von microsoft gemacht wird und ich hier nur den Simpelsten aller Compiler bau.

      Aber die Compiler von anderen sind ähnlich aufgebaut nur das sie das ganz in C/C++ umwandeln und dann mit GCC o. ä. Compelieren.

      Theoretisch könnte ich hier auch nen richtigen Compiler schreiben aber das würde zu weit führen und hätte schon das Ausmaß eines Buches Oo
      Was den Compilerbau angeht bin ich gerade auf das "Roslyn" CTP gestoßen:
      blogs.msdn.com/b/kirillosenkov…011/10/18/roslyn-ctp.aspx
      codeproject.com/Articles/30259…ree-Introductory-Projects

      Sieht recht vielversprechend aus und kann bestimmt behilflich sein.
      Von meinem iPhone gesendet
      wen jetzt versuche einen neuen befahl einzubauen z.b.

      VB.NET-Quellcode

      1. Case "TITLE"
      2. Dim buf As String
      3. For i As Integer = 1 To token.Length - 1
      4. If (token(i).Substring(0, 1) = "$") Then
      5. buf += Chr(34) & " & " & token(i).Substring(1) & " & " & Chr(34)
      6. Else
      7. buf += token(i) & " "
      8. End If
      9. Next
      10. Me.endCode.Append("Console.Title(" & Chr(34) & buf & Chr(34) & ")")


      kommt nen error:

      Quellcode

      1. C:\Users\NicoNeu\AppData\Local\Temp\zxg2ut31.0.vb(4,0) : error BC30545: Eigenschaftenzugriff muss der Eigenschaft zugewiesen werden oder deren Wert verwenden.


      wie kann man das ändern?

      VB.NET-Quellcode

      1. Case "sp"
      2. Me.endCode.Append("Dim " & token(1) & " As New System.IO.Ports.SerialPort")
      3. Me.endCode.Append(token(1) & ".BaudRate = 9600")
      4. Me.endCode.Append(token(1) & ".Portname = " & token(2))
      5. Me.endCode.Append(token(1) & ".Open()")
      6. Me.endCode.Append(token(1) & ".Write(Chr(" & token(3) & "))")
      7. Me.endCode.Append(token(1) & ".Close()")


      Es wird eine "End-Of Anweisung" erwartet?
      Wo brauche ich eine end-of anweisung? :O