-
Notifications
You must be signed in to change notification settings - Fork 0
2 OmegaPrint
Hier wollen wir die Frage klären, wie wir Ohm/S für OmegaPrint nutzen.
2020 bauen wir auf diesem Ohm/S Fork auf.
Mittels des OPUserInterface fügen wir über die contentsSymbolQuints
einen Button in den Browser hinzu. Dafür gibt es Extension-Methods im CodeHolder. Zum Beispiel die Methode CodeHolder >> sourceStringPrettifiedAndDiffed
, indem OPPrinter als möglicher PrettyPrinter hinzugefügt wird. Auch im MessageSet
sind einige Extension-Methods, welche unter anderem dafür sorgen, dass dort auch der Code, wenn man nach Methoden im Image sucht, auch gehighlightet wird.
Wie man mit Ohm/S eine Methode parsed, habt ihr bereits in 1 Ohm S gesehen. Die Klasse OPPrinter bietet dem Browser das Interface OPPrinter >> format:in:notifying:
. Dort bekommen wir den String und in OPPrinter >> evaluate:startingFrom:
findet ihr die bereits bekannten Methodenaufrufe des Parsings. Nachdem wir das MatchResult haben, rufen wir auf dem evaluator value:
auf. Dort wertet dann ein OPSourceRewriter den übergebenen CST aus. Dieser SourceRewriter benötigt dazu einen OPCommentExtractor. Der SourceRewriter die Klasse, welche mittels Ohm diesen Baum traversiert. So bietet der OPPrinter das Interface und der OPSourceRewriter kann CSTs mithilfe des OPCommentExtractor s auswerten.
In Ohm ist ein impliziter Visitor implementiert. Dieser ist nicht so leicht zu finden, wird aber durch die Methode value:
aufgerufen. Wenn ihr dabei euch die Methode OhmAttributes >> value:
anschaut, seht ihr, dass unter anderem geschaut wird, ob es in der gerade genutzen Evaluatorklasse eine Methode gibt, deren Name genau zu dem der Regel passt.
Daher kommt es, dass in der OPSourceRewriter-Klasse so viele großgeschriebene Methoden ohne Sender sind. Diese Methoden müssen also genau den Methodennamen haben, wie ihre zugehörige Regel. Weiterhin müssen sie passend viele Argumente nehmen. Mittels perform:withArguments:
wird die entsprechende Regel (wenn vorhanden) aufgerufen.
Um das genauer zu verstehen, schauen wir uns die Regel NestedExpression an.
NestedExpression =
"(" Statement ")"
Dabei sehen wir, dass diese Regel drei Bestandteile hat. Eine NestedExpression besteht also aus eine öffnenden Klammer, einem Statement und einer schließenden Klammer. Wir möchten jetzt definieren, dass nach einer öffnenden und vor einer schließenden Klammer kein Leerzeichen sein soll. Daher schreiben wir eine Methode, welche genau den Namen "NestedExpression" hat und außer sich selbst noch 3 Argumente nimmt.
OPPrinter >> NestedExpression: aNode with: aTerminal and: aStatement and: anotherTerminal
^ '(' , (self value: aStatement) , ')'
aTerminal und anotherTerminal sollten hier also auch bereits die Klammern enthalten, während wir in aStatement das Statement zwischen den Klammern übergeben bekommen. Da wir allerdings wissen, dass eine NestedExpression Klammern hat, können wir diese auch explizit setzen. So geben wir jetzt also explizit an, dass es keine Leerzeichen geben soll. Dann werten wir das Statement zwischen den Klammern rekursiv mittels value: aus.
Wenn in der Methode OhmAttributes >> value:
keine Methode mit passender Signatur gefunden wird, wird eine von 3 speziellen Regeln aufgerufen. Dies passiert in der Methode OhmAttributes >> tryToUseSpecialAttributesFor:asMessage:on:
. Die entsprechenden Methoden werden in den folgenden drei Abschnitten erklärt und auch in dieser Reihenfolge überprüft. Falls keine dieser Methoden gültig ist, wird ein Fehler geworfen.
terminalExpression sind Knoten, die genau ein Zeichen enthalten. Hier geben wir einfach dieses Zeichen als String zurückgibt.
listExpression sind Knoten im CST mit denen Wiederholungen (*
und +
) abgebildet werden können. Sie enthalten immer eine Liste von gleichartigen Knoten. Wir nutzen die listExpression von OhmSourceRewriter, welche alle Listen abdeckt, deren Kinder direkt aneinander gehängt werden (Elternknoten ist lexikalischer Knoten) oder durch Leerzeichen abgetrennt werden (Elternknoten ist syntaktischer Knoten).
Nicht für jede Regel in der Grammatik haben wir so eine zugehörige Methode implementiert. Damit diese dennoch ausgewertet werden können, gibt es allerdings die defaultExpression:. Von dieser gibt es verschiedene Versionen wie beispielsweise OhmSourceRewriter >> defaultExpression:
oder OhmConservativeSourceRewriter >> defaultExpression:
. Diese ist sehr nützlich, da man so nicht gezwungen ist jede Regel von Anfang an zu implementieren oder auch einfache Auswertungen übernimmt, die nicht speziell formatiert werden müssen. Wir haben uns mittlerweile eine eigene defaultExpression geschrieben, mit der wir praktisch alle lexikalischen Regeln und die syntaktischen Regeln, deren Kinder durch Leerzeichen voneinander getrennt sein sollen, formatieren können.
Vielleicht habt ihr ihn ja bereits gefunden: den OhmSmalltalkSourceRewriter. Er liegt im Paket OhmSemantics und vom Namen her klingt das erstmal sehr hilfreich und vielversprechend. Dennoch haben wir dann im Laufe der Zeit festgestellt, dass wir alle Methoden, die dieser Rewriter hat, selbst anders implementiert haben und daher erbt OPPrinter vom OhmSourceRewriter.
Um gegebenenfalls Leerzeilen und Kommentare aus dem Quellcode zu übernehmen, haben wir zwischenzeitlich auch überlegt, ob die defaultExpression: des OhmConservativeSourceRewriter s weniger viel kaputt macht, aber das hatte nicht geholfen.
Wie bereits kurz erwähnt, tauchen keine Leerzeichen und Kommentare im CST auf. Wenn man allerdings eine Methode nur formatieren möchte, ohne sie zu sehr zu verändern, ist das offensichtlich ein Problem. Es gibt zu jedem Knoten das Intervall und man hat weiterhin Zugriff auf den Eingangsstream, sodass man theoretisch bereits darüber Kommentare zurückrechnen könnte. Dies ist jedoch nicht so einfach wie gedacht. Eine Diskussion zu der Entwicklung findet ihr in den Issues von Ohm/S.
Mit der Methode OhmNode >> skippedSpacesNodes
kann man sich OhmNodes zurückgeben lassen, welche dann wieder mit value: traversiert werden können. Diese OhmNodes enhalten somit alle spaces und comments, welche vor diesem Knoten liegen, auf welchem wir die Methode aufrufen.
Um dies bei jedem Knoten machen zu können, haben wir eine eigene value:-Methode implementiert.