Ergebnis 1 bis 20 von 25

Thema: Wie entsteht eine neue Programmiersprache?

Hybrid-Darstellung

Vorheriger Beitrag Vorheriger Beitrag   Nächster Beitrag Nächster Beitrag
  1. #1
    Geht es darum wie ein Computer aus Einsen und Nullen komplexere Befehle ausführt, oder um den Aufbau eines Compilers?
    Das wird glücklicherweise mittlerweile gut getrennt.
    Der CPU-Hersteller gibt einen bestimmten Befehlssatz vor, oft mit nur rudimentären Operationen (Speicherzugriffe, Einfache Rechenoperationen, Shiften und ein paar Sprunganweisungen).
    Für einen Compiler ist das natürlich das begehrte Zielformat.

    Der Schritt vom Quellcode zum fertigen Programm ist relativ komplex und wird daher in verschiedene Schritte unterteilt.
    1. Lexing
    Der Quellcode wird mithilfe von regulären Ausdrücken in eine Reihe von synaktischen Tokens zerlegt. Statt eines Zeichenstroms kriegt der nachfolgende Parser die einzelnen syntaktischen Bausteine Stück für Stück. (Statt, "if x > 1 then y" liest der parser: if-token, identifier, greater, constant, then, identifier)
    In dem Schritt werden auch Whitespaces und Kommentare "überlesen".
    2. Parser
    Der Parser arbeitet auf diesem Tokenstream des Lexers und muss entscheiden ob der Input ein syntaktisch korrektes Programm darstellt oder nicht. Hier wird mit kontextfreien Grammatiken (bzw. mit etwas eingeschränkten kfG) gearbeitet. Der Parser baut als Output einen Syntax-Baum auf (genau genommen einen sogenanntne "Abstrakten Syntax Baum" AST, da der konkret zum Parsen verwendete Syntaxbaum für die spätere Arbeit etwas unhandlich ist)

    In der Regel spricht man direkt den Parser an, der dann jeweils den Lexer aufruft um das nächste Token zu kriegen. Da Parsing ein relativ komplexes Problem ist baut man mittlerweile eher selten Parser von Hand. Stattdessen gibt es Programme, die für eine gegebene Grammatik einen entsprechenden Parser oder Lexer (oder beides) generieren.

    Ich benutz jetzt mal eine einfache Anweisung wie "y = x + 2" als Beispiel.
    Im AST hätten wir also hier einen Zuweisungsknoten, dessen linkes Kind ein Identifier Knoten wäre (die variable y), der rechte Knoten wäre ein Add-Knoten, mit den jeweiligen Kindern: Identifier-Knoten (variable x) und Konstante-Knoten (die 2).
    Auf diesem AST kann man nun Typchecking (stimmen die typen der kinder des add-knoten für eine addition, ist der ergebene typ des Add-Knotens kompatibel mit dem typ von y) und diverse Optimierungen vornehmen.

    Am Ende ist es relativ "simpel" für einen bestimmten Knoten im AST den entsprechenden Assemblercode zu generieren. Für obiges Beispiel wäre das zB:
    1. Lade die Adresse des linken Kinderknoten (des Ziels) in ein Register A
    2. Führe den Code zur Berechnung des Rechten Kinderknotens aus (Ergebnis steht in einem fest definierten Register B)
    3. Kopiere den Wert aus B an die Adresse, die in A steht

    Ich hab auf der Platte noch den Compiler (Source Code) für eine einfache Programmiersprache liegen, den wir letztes Semester als Projekt schreiben mussten. Der Compiler ist in Java geschrieben und erzeugt als Output Code für einen Assembler, der das ganze in Java-bytecode übersetzt. Falls du Interesse hast, kannst du ihn gerne haben =).

  2. #2
    Sehr detailierte Antworten, vielen Dank.
    Ich denke ich konnte meinen Kopf damit ein klein wenig erleichtern.

    Und vielen Dank für das Angebot, allerdings denke ich ist dies nicht notwendig MagicMagor.

  3. #3
    Ich wäre allerdings durchaus interessiert

  4. #4
    Da wirft sich mir allerdings doch noch eine Frage auf.
    Läuft ein Interpreter mit dem selben Prinzip? Es müsste doch noch ein klein wenig anders sein schätze ich.
    Wird eine Zeile nach der anderen Compiliert? Oder der Befehl welcher gerade ausgeführt wird? Oder die ganze Methode die gerade ausgeführt wird?
    Denn falls es nur Zeilen / Funktionsweise compiliert werden würde dann müssten if/then/else Strukturen etwas komplizierter ausfallen oder täusche ich mich?

  5. #5
    Die Compiler arbeiten quasi immer nach dem selben Schema.
    If-then-else sind nichts weiteres als sog. Sprungbefehle. Sie springen von einem Speicherbereich zu nächsten.
    Du kennst vielleicht den goto-Befehl aus einigen Sprachen. If-then-else ist im Grunde nichts anderes, als ein goto-Befehl.

    Ein Compiler ist ja kein Zeileninterpreter, wie bei Scriptsprachen, sondern er übersetzt das komplette Programm. Wenn das Programm gestartet wird, wird das Programm in den Arbeitsspeicher geladen und dort ausgeführt. Somit gibt es für den Prozessor auch zwischen Variable und Funktion keinen Unterschied. Beides sind nur Bereiche im Speicher, die der Prozessor abarbeitet. Vielleicht hast du auch schonmal was von (Funktions-)Zeigern gehört. Also ein Zeiger, der auf eine Variable oder Funktion zeigen.

    Zeileninterpreter interpretieren nur die Zeile, in der sich sich grad befinden. Bei If-Then-Else würde er dann mehrere Zeilen, zum Else-teil überspringen, wenn die Bedingung nicht erfüllt ist.

  6. #6
    Vielen Dank, ganz genau scheine ich das aber noch nicht verstanden zu haben glaube ich.
    Was bedeutet dies konkret bei einer Programmiersprache wie beispielsweise Ruby (welche in dem RPG-Maker XP verwendet wird), der Code wird nunmal nicht compiliert wie in beispielsweise C++, wenn man das Programm startet was passiert dann damit? Wird der Code Zeile für Zeile eingelesen?

  7. #7
    Ich weiß nicht ob man tatsächlich einen Interpreter schreiben kann der Zeile für Zeile einliest und direkt ausführt.
    Ich wühle mich gerade für meine BA-Arbeit durch den Code des Orc-Interpreters und muss einen entsprechenden Interpreter in Prolog schreiben. Orc liest das Programm erst einmal komplett ein und baut den kompletten AST auf. Dadurch können zB. Syntaxfehler sofort erkannt werden. Auch semantische Überprüfungen wie Typechecking und ob ungebundene Variablen vorkommen erledigt Orc direkt vor der Ausführung (das könnte man aber natürlich auch erst im laufenden Betrieb machen).
    Der Interpreter durchläuft dann letzlich den AST, fängt beim Wurzelknoten an und führt je nach Knotentyp bestimmte Aktionen aus. Klingt für mich nach der besten Methode einen Interpreter zu schreiben.

    @dfyx
    Du hast gleich Post

  8. #8
    Zitat Zitat von MagicMagor Beitrag anzeigen
    Ich weiß nicht ob man tatsächlich einen Interpreter schreiben kann der Zeile für Zeile einliest und direkt ausführt.
    BASIC oder die Linux Shell besitzen einen Zeileninterpreter, die Befehl für Befehl einlesen und ausführen.
    Am Besten demonstriert dies der C64, da dort die Benutzeroberfläche ein BASIC Interpreter war. Gleichzeitig war die Benutzeroberfläche auch eine Entwicklungsumgebung.

  9. #9
    Aber wenn der Interpreter vor dem Programm Start ersteinmal das gesamte Programm einliest und überprüft, wo genau liegt dann der Unterschied zu den Compilern, außer, dass die Compiler diese Arbeit nur einmal zu erledigen haben?

  10. #10
    Geschwindigkeit.Ein Zeileninterpreter muss die Zeile parsen und dann in Maschinensprache umwandeln und erst dann kann der Befehl ausgeführt werden. Ein Compiler hingegen hat nun alle Zeit der Welt, um den Programmcode zu analysieren. Die meisten Compiler können dann noch den Quellcode optimieren, sodass er noch ein tick schneller arbeitet. Wenn z.B.
    Code:
    a = 1;
    a = a + 4;
    im Quelltext steht, dann macht der Compiler daraus
    Code:
    a=5;
    (um jetzt mal ein einfaches Beispiel zu nennen)
    Somit muss das Programm nicht zwei Befehle ausführen, sondern nur einen. Eine Addition besteht ja auch noch aus mehreren Prozessinstruktionen.

    Ein Interpreter läuft auch im Hintergrund, um den Quelltext zu interpretieren. Bei einem compilierten Programm läuft nichts im Hintergrund, was dann noch Ressourcen einspart, weil ein Interpreter ja auch seine Rechenzeit und sein Speicher benötigt.

  11. #11
    Vielen Dank für die Antwort, allerdings hatte ich mich dabei eher auf den Beitrag von MagicMagor bezogen.
    Wo der Sinn eines Zeileninterpreters liegt ist mir ja einleuchtend, allerdings, wenn der Interpreter sowieso den gesamten Code vor dem Start durchgeht so wie MagicMagor es beschrieb, oder ich es zumindest verstanden habe, dann liegt doch der Unterschied, wenn überhaupt, lediglich auf einer graduellen Ebene oder irre ich mich da?

  12. #12
    Zitat Zitat
    dann liegt doch der Unterschied, wenn überhaupt, lediglich auf einer graduellen Ebene oder irre ich mich da?
    Jein. Der Geschwindigkeitsvorteil von (gut) compiliertem Code zu Interpreter ist beachtlich. Du musst bedenken, der Interpreter durchläuft beim Ausführen den Syntaxbaum, hat also einiges an Verwaltungsaufwand und selbst bei der Ausführung eines einzelnen Knoten kommen zusätzliche Befehle hinzu.
    Wo der Compiler also vielleicht 3-5 Maschinenanweisungen für einen Knoten produziert, laufen beim Interpreter ungleich mehr Anweisungen für denselben Knoten ab. Hinzu kommt, dass für bestimmte Anwendungen eventuell spezielle Maschinenanweisungen bereit stehen, die das ganze noch effizienter und schneller ausführen, der Compiler kann gezielt solche Anweisungen statt "normaler" Anweisungen nutzen, beim Interpreter ist das nicht unbedingt möglich.
    Der Compiler vollführt beim Compilieren eine Unmenge an Analyse- und Optimierungsaufwand der sich in einer langsamen Ausführung äussern. Wenn du mal ein größeres Projekt selbst compiliert hast (zB Open Office) weißt du was das heißt. Dieser Aufwand muss aber nur 1 mal gemacht werden, nämlich beim Entwickler der seinen Code compiliert - der ausführbare Code läuft deswegen aber sehr flott.
    Wenn der Interpreter diese ganzen Analysen auch machen würde, sobald er ein Programm einliest, könnte er vielleicht die Ausführung des Codes beschleunigen, würde aber mitunter minutenlang rechnen, bevor er mit der Auführung beginnt - was in der Praxis natürlich undenkbar ist.
    Der Interpreter hat dabei noch das Problem, das das Interpreterprogramm anwesend sein muss wenn man das Programm ausführen muss, ebenso wie der zu interpretierende Code - beim compilierten Programm braucht man nur das Programm, das man ausführen will.

    Ein weiterer Vorteil des Compilers ist das compilieren in andere Zielformate. Du kannst aus demselben Code, sofern der Compiler es unterstützt, ausführbare Programme für ganz unterschiedliche Plattformen produzieren, von anderen Betriebssystemen bis hin zu gänzlich anderer Hardware. Beim Interpreter müsstest du den kompletten Interpreter für die Zielplattform portieren - ein ziemlicher Aufwand.

    Java versucht aber genau so etwas, eine Art Verbindung von beidem. Java-Code wird in ein Zwischenformat compiliert, den sogenannten Bytecode. Dieses "Java-Programm" wird dann beim Benutzer durch die Java Virtual Machine (JVM) zur Laufzeit interpretiert. Der Vorteil des Entwickler ist, das sein Code in ein portables Format umgewandelt wird, er also die Besonderheiten der Zielplattform nicht beachten muss - compile once, run everywhere.
    Nachteil ist, dass der Benutzer dafür sorgen muss, das er eine passende JVM installiert hat, die natürlich auch jede Menge Ressourcen schluckt und Java-Code ist trotz aller Optimierungen immer noch deutlich langsamer als nativ compilierter C(++) Code. Gerade aktuelle Top-Spiele brauchen aber Geschwindigkeit, Java fällt dort alleine deswegen schon durch.

    Der Geschwindigkeitsvorteil mag sich nur "graduell" unterscheiden, ist aber in der Praxis bei einigen Anwendungen gravierend. Ein Spaziergang unterscheidet sich ja auch nur "graduell" von einem olympischen 100-Meter Sprint - eigentlich wird bei beiden ja dasselbe gemacht, oder?

Berechtigungen

  • Neue Themen erstellen: Nein
  • Themen beantworten: Nein
  • Anhänge hochladen: Nein
  • Beiträge bearbeiten: Nein
  •