MQL-Parsing mit Hilfe von MQL

30 April 2019, 09:35
Stanislav Korotky
0
158

Einführung

Programmierung ist im Wesentlichen die Formalisierung und Automatisierung einiger Prozesse mit Hilfe von Mehrzweck- oder Spezialsprachen. Die Handelsplattform MetaTrader ermöglicht die Anwendung der Programmierung zur Lösung einer Vielzahl von Problemen eines Händlers unter Verwendung der integrierten MQL-Sprache. In der Regel basiert der Codierungsprozess auf der Analyse und Verarbeitung von Anwendungsdaten nach den Regeln des Quellcodes. Manchmal ergibt sich jedoch die Notwendigkeit, die Quellcodes selbst zu analysieren und zu verarbeiten. Hier sind einige Beispiele.

Eine der konsistentesten und beliebtesten Aufgaben ist die kontextuelle und semantische Suche in der Quellcodebasis. Sicherlich können Sie in einem Quellcode wie in einem normalen Text nach Zeichenketten suchen, aber die Semantik des Gesuchten geht verloren. Schließlich ist es im Falle von Quellcodes wünschenswert, zwischen den Besonderheiten der Verwendung der Teilzeichenkette in jedem konkreten Fall zu unterscheiden. Wenn ein Programmierer herausfinden möchte, wo eine bestimmte Variable verwendet wird, wie z.B. "notification", dann kann eine einfache Suche nach ihrem Namen viel mehr als nötig zurückgeben, wenn die Zeichenkette in anderen Werten, wie beispielsweise einem Methodennamen oder einem Literal, oder in Kommentaren vorkommt.

Komplizierter und in der Regel bei größeren Projekten gefragt ist die Aufgabe, Codestruktur, Abhängigkeiten und Klassenhierarchie zu identifizieren. Es ist eng mit der Metaprogrammierung verbunden, die es ermöglicht, Code-Überarbeitungen/-Verbesserung und die Codegenerierung durchzuführen. Erinnern Sie sich daran, dass der MetaEditor einige Möglichkeiten bietet, Code zu generieren, insbesondere die Quellcodes von Expert Advisors mit dem Wizard zu erstellen oder eine Header-Datei aus dem Quellcode zu erstellen. Die Potenziale dieser Technologie sind jedoch viel größer.

Die Code-Strukturanalyse ermöglicht die Berechnung verschiedener Qualitätsmetriken und Statistiken sowie das Auffinden der typischen Quellen von Laufzeitfehlern, die der Compiler nicht erkennen kann. Tatsächlich ist der Compiler selbst natürlich das erste Werkzeug zur Analyse eines Quellcodes und gibt viele unterschiedliche Warnungen zurück; die Überprüfung auf alle möglichen Fehler ist jedoch in der Regel nicht eingebaut — diese Aufgabe ist zu umfangreich, so dass sie normalerweise separaten Programmen zugeordnet ist.

Darüber hinaus wird das Parsen von Quellcodes für das Styling (Formatierung) und die Verschleierung (Verwirrung) verwendet.

Viele Werkzeuge, die die oben genannten Probleme implementieren, stehen industriellen Programmiersprachen zur Verfügung. Im Falle von MQL ist die Auswahl begrenzt. Wir können versuchen, die Analyse von MQL mit den verfügbaren Mitteln einzurichten, indem wir MQL auf die gleiche Ebene mit C++ stellen. Es funktioniert ziemlich einfach mit einigen Werkzeugen, wie z.B. Doxygen, erfordert aber eine tiefere Anpassung für mächtigere Mittel, wie z.B. lint, da MQL eben noch nicht C++ ist.

Es sei darauf hingewiesen, dass dieser Artikel sich nur mit statischer Codeanalyse beschäftigt, während es dynamische Analysatoren gibt, die es Ihnen ermöglichen, den Überblick über Speicherbedienungsfehler, Workflow-Sperren, die Richtigkeit der Werte von Variablen und vieles mehr in einer virtuellen Umgebung zu behalten.

Für die statische Analyse eines Quellcodes können verschiedene Ansätze verwendet werden. Bei einfachen Problemen, wie z.B. der Suche nach Eingabevariablen eines MQL-Programms, genügt die Verwendung der Bibliothek mit regulären Ausdrücken. Generell muss die Analyse auf einem Parser unter Berücksichtigung der MQL-Grammatik basieren. Es ist dieser Ansatz, den wir in diesem Artikel berücksichtigen werden. Wir werden auch versuchen, ihn praktisch anzuwenden.

Mit anderen Worten, wir werden einen MQL-Parser in MQL schreiben und die Metadaten des Quellcodes erhalten. Dies wird es uns sowohl ermöglichen, die oben genannten Probleme zu lösen, als auch einige andere fantastische Herausforderungen im Folgenden anzubieten. Da wir also einen völlig korrekten Parser haben, könnten wir uns darauf verlassen, einen MQL-Interpreter zu entwickeln oder MQL automatisch in andere Handelssprachen zu konvertieren und umgekehrt (das sogenannte Transpiling). Allerdings habe ich den Begriff "fantastisch" aus einem bestimmten Grund verwendet. Obwohl all diese Techniken bereits in anderen Bereichen weit verbreitet sind, müssen wir zunächst einen Einblick in die Grundlagen gewinnen, um sie auf der MetaTrader-Plattform anzugehen


Technologischer Überblick

Es gibt viele verschiedene Parser. Wir werden nicht auf technische Details eingehen — Sie finden einführende Informationen bei Wikipedia, und eine große Menge an Quellen wurde für fortgeschrittene Studien entwickelt.

Wir sollten nur beachten, dass der Parser auf der Grundlage der sogenannten sprachbeschreibenden Grammatik funktioniert. Eine der häufigsten Formen der Grammatikbeschreibung ist die von Backus-Naur (BNF). Es gibt zahlreiche BNF-Modifikationen, aber wir werden nicht allzu viele Details aufgreifen und nur die grundlegenden Punkte berücksichtigen.

In BNF werden alle Sprachstrukturen durch die sogenannten Non-Terminale definiert, während unteilbare Einheiten Terminale sind. "Terminal" bezeichnet hier den letzten Punkt beim Parsen eines Textes, d.h. ein Token, das ein Fragment des Quellcodes "wie besehen" enthält, und das als Ganzes interpretiert wird. Dies kann z.B. das Kommazeichen, eine Klammer oder ein einzelnes Wort sein. Die Liste der Terminale, d.h. das Alphabet, definieren wir selbst innerhalb der Grammatik. Basierend auf einigen Regeln bestehen alle anderen Komponenten des Programms aus Terminalen.

Beispielsweise können wir wie folgt festlegen, dass das Programm aus Operatoren in der vereinfachten BNF-Notation besteht:

program ::= operator more_operators
more_operators ::= program | {empty}

Hier wird gesagt, dass das Non-Terminal 'programm' aus einem oder mehreren Operatoren bestehen kann, wobei die nachfolgenden Operatoren durch eine rekursive Verknüpfung mit "Programm" beschrieben wurden. Das Zeichen '|' (ohne Anführungszeichen in BNF) bedeutet logisches ODER — die Auswahl einer der Optionen. Um die Rekursion abzuschließen, wird im obigen Eintrag ein spezielles Terminal {empty} verwendet. Sie kann als leere Zeichenkette oder als Option zum Überspringen der Regel dargestellt werden.

Das Zeichen 'operator' ist ebenfalls ein Non-Terminal und erfordert ein 'expanding' über andere Non-Terminale und Terminale, wie beispielsweise dieses:

operator ::= name '=' expression ';'

Dieser Eintrag definiert, dass jeder Operator mit dem Variablennamen beginnt, dann mit dem Zeichen '=', einem Ausdruck und der Operator endet mit dem Zeichen ';'. Die Zeichen '=' und ';' sind Terminale. Der Name besteht aus Buchstaben:

name ::= letter more_letters
more_letters ::= name | {empty}
letter ::= [A-Z]

Hier kann jedes beliebige Zeichen von 'A' bis 'Z' als Buchstabe verwendet werden (ihre Menge wird mit eckigen Klammern angegeben).

Der Ausdruck soll aus Operanden und Arithmetik (Operationen) gebildet werden:

expression ::= operand more_operands
more_operands ::= operation expression | {empty}

Im einfachsten Fall besteht ein Ausdruck aus nur einem Operanden. Es können jedoch auch mehrere von ihnen (more_operands) sein, dann werden sie über das Operationszeichen als Unterausdruck an sie angehängt. Operanden sollten in der Lage sein, Referenzen auf Variablen oder Zahlen zu sein, während Operationen '+' oder '-' sein können:

operand ::= name | number
number ::= digit more_digits
more_digits ::= number | {empty}
digit ::= [0-9]
operation ::= '+' | '-'

So haben wir die Grammatik einer einfachen Sprache beschrieben, in der Berechnungen mit Zahlen und Variablen durchgeführt werden können, wie z.B.:

A = 10;
X = A - 5;

Wenn wir anfangen, den Text zu analysieren, dann prüfen wir in der Tat, welche Regeln funktionieren und welche davon fehlschlagen. Diejenigen, die gearbeitet haben, sollten früher oder später 'production' produzieren — ein Terminal finden, das mit dem Inhalt an der aktuellen Position im Text übereinstimmt, wobei der Cursor an die nächste Position bewegt wird. Dieser Vorgang wird so lange wiederholt, bis der gesamte Text den Non-Terminal fragmentweise zugeordnet ist und eine von der Grammatik erlaubte Reihenfolge bildet.

Im obigen Beispiel beginnt der Parser, der das Zeichen 'A' an seiner Eingabe hat, die Regeln wie unten beschrieben anzuzeigen:

program
  operator
    name
      letter
        'A'

und findet die erste Übereinstimmung. Der Cursor springt zum nächsten Zeichen '='. Da 'letter' ein Buchstabe ist, kehrt der Parser zurück in die Regel 'name'. Da 'name' nur aus Buchstaben bestehen kann, funktioniert die Option more_letters nicht (sie ist gleich {empty}), und der Parser kehrt in die Regel 'operator' zurück, wo es das Terminal '=' gibt, das dem Namen folgt. Dies wird die zweite Entsprechung sein. Dann, wenn die Regel 'expression' erweitert wird, findet der Parser den Operanden — ganze Zahl 10 (als eine Folge von zwei Ziffern), und schließlich vervollständigt Semikolon das Parsen der ersten Zeile. Nach den Ergebnissen werden wir in der Tat den Variablennamen, den Inhalt des Ausdrucks, nämlich die Tatsache, dass er aus einer Zahl besteht, und seinen Wert kennen. Die zweite Zeile wird in ähnlicher Weise analysiert.

Es ist wichtig zu beachten, dass die Grammatik ein und derselben Sprache auf unterschiedliche Weise aufgezeichnet werden kann, wobei letztere formal aufeinander abgestimmt sind. In einigen Fällen kann die Einhaltung der Regeln jedoch zu gewissen Problemen führen. Beispielsweise könnte die Beschreibung einer Nummer wie folgt dargestellt werden:

number ::= number digit | {empty}

Dieses Eingabeformat trägt den Namen linke Rekursion: Das Non-Terminal 'number' befindet sich sowohl im linken als auch im rechten Teil und bestimmt die Regeln seiner 'production', wobei es das allererste, linke, in der Zeichenkette ist (daher der Name von "linke Rekursion"). Dies ist die einfachste, explizit linke Rekursion. Es kann jedoch implizit sein, wenn das Non-Terminal nach einigen Zwischenregeln in eine Zeichenkette erweitert wird, die mit diesem Non-Terminal beginnt.

Die linke Rekursion tritt oft in formalen BNF-Notationen der Grammatik von Programmiersprachen auf. Einige Arten von Parsern können jedoch, je nach ihrer Implementierung, in einer Schleife mit ähnlichen Regeln stecken bleiben. Betrachten wir die Regel als Handlungsanweisung (Parsing-Algorithmus), dann wird dieser Eintrag die "Zahl" immer wieder rekursiv eingeben, ohne neue Terminale aus dem Input-Stream zu lesen, was theoretisch bei der Erweiterung der Non-Terminal 'digit' geschehen sollte.

Da wir versuchen, die MQL-Grammatik nicht von Grund auf neu zu erstellen, sondern nach Möglichkeit die BNF-Notationen der C++-Grammatik zu verwenden, muss auf linke Rekursionen geachtet werden, und die Regeln sollen auf alternative Weise neu geschrieben werden. Gleichzeitig müssen wir auch den Schutz vor Endlosschleifen implementieren — wie wir weiter sehen werden, sind die Grammatiken der Sprachen vom Typ C++ oder MQL so verzweigt, dass es unmöglich zu sein scheint, sie manuell auf ihre Richtigkeit zu überprüfen.

Hier ist es wichtig zu beachten, dass das Schreiben von Parsern eine echte Wissenschaft ist, und es wird empfohlen, mit der schrittweisen Beherrschung dieses Bereichs auf einer Basis "von einfach zu komplex" zu beginnen. Das einfachste ist das Parsen mittels eines Rekursiven Abstiegs. Es wird der Eingabetext als Ganzes betrachtet, beginnend mit dem Non-Terminal der Grammatik. Im obigen Beispiel war das das Non-Terminal 'program'. Nach jeder geeigneten Regel überprüft der Parser die Reihenfolge der Eingabezeichen auf Übereinstimmung mit den Terminalen und bewegt sich beim Auffinden der Übereinstimmungen durch den Text. Wenn der Parser zu irgendeinem Zeitpunkt des Parsing eine Abweichung findet, kehrt er zu den Regeln zurück, in denen eine Alternative angegeben wurde, und überprüft somit alle möglichen Sprachstrukturen. Dieser Algorithmus wiederholt vollständig die Operationen, die wir im obigen Beispiel rein theoretisch durchgeführt haben.

Der Vorgang des "Roll-back" wird als "Backtracking" bezeichnet und kann sich negativ auf die Reaktionsgeschwindigkeit auswirken. So erzeugt ein klassischer Abstiegsparser im schlimmsten Fall eine exponentiell wachsende Anzahl von Optionen beim Betrachten des Textes. Es gibt verschiedene Möglichkeiten, dieses Problem zu lösen, wie z.B. einen Vorhersageparser, der kein Backtracking benötigt. Die Laufzeit wird dann linear.

Dies ist jedoch nur für Grammatiken möglich, bei denen die Regel der 'production' durch eine vordefinierte Anzahl der nachfolgenden Zeichen k eindeutig ausgewählt werden kann. Solche fortgeschritteneren Parser basieren in ihrem Betrieb auf speziellen Übergangs-Tabellen, die im Voraus nach allen Regeln der Grammatik berechnet werden. Sie beinhalten, sind aber nicht beschränkt auf, LL-Parser und LR-Parser.

LL steht für Left-to-right, Linksableitung. Das bedeutet, dass der Text von links nach rechts betrachtet wird, und die Regeln auch von links nach rechts, was einer Top-Down-Schlussfolgerung (von allgemein bis spezifisch) entspricht, und in diesem Sinne ist LL ein Verwandter unseres Abstiegsparsers.

LR steht für Left-to-right, Rechtsableitung. Das bedeutet, dass der Text wie bisher von links nach rechts betrachtet wird, aber die Regeln von rechts nach links, was der Bottom-up-Bildung der Sprachstrukturen entspricht, d.h. von einzelnen Zeichen bis hin zu größeren und größeren Non-Terminale. Außerdem hat LR weniger Probleme mit der linken Rekursion.

In den Namen der Parser LL(k) und LR(k) wird in der Regel die Anzahl der Zeichen k als Lookahead angegeben, bis zu der sie den Text vorwärts betrachten. In den meisten Fällen ist die Wahl von k = 1 ausreichend. Dieses Ausreichend ist jedoch lückenhaft. Es geht darum, dass viele moderne Programmiersprachen, einschließlich C++ und MQL, keine Sprachen mit einer kontextfreien Grammatik sind. Mit anderen Worten, die gleichen Fragmente des Textes können je nach Kontext unterschiedlich interpretiert werden. In solchen Fällen reicht es in der Regel nicht aus, eine Entscheidung über die Nachricht über das Geschriebene zu treffen, ein oder sogar eine beliebige Anzahl von Zeichen, da Sie den Parser mit anderen Tools, wie z.B. Präprozessor oder Symboltabelle (die Liste der bereits erkannten Identifikatoren und deren Bedeutung), verknüpfen müssen.

Für die C++-Sprache gibt es einen prototypischen Fall von Mehrdeutigkeit (sie passt auch zu MQL). Was bedeutet der folgende Ausdruck?

x * y;

Dies kann das Produkt der Variablen x und y sein, aber es kann auch die Beschreibung der Variablen y als x-artiger Pointer sein. Lassen Sie sich nicht in Verlegenheit bringen, dass das Produkt der Multiplikation, wenn es sich um eine Multiplikation handelt, nirgendwo gespeichert wird, da der Operator der Multiplikation überladen sein kann und Nebenwirkungen haben könnte.

Ein weiteres Problem, unter dem die meisten C++-Compiler in der Vergangenheit litten, war die Mehrdeutigkeit bei der Interpretation von zwei aufeinanderfolgenden Zeichen '>'. Es geht darum, dass bei der Einführung der Templates Strukturen des folgenden Typs im Quellcode zu erscheinen begannen:

vector<pair<int,int>> v;

wobei die Sequenz '>>' zunächst als Shift-Operator definiert wurde. Eine Zeit lang, bis ein feinerer Prozess für solche speziellen Fälle eingeführt wurde, mussten wir ähnliche Ausdrücke mit Leerzeichen schreiben:

vector<pair<int,int> > v;

In unserem Parser müssen wir dieses Problem auch umgehen.

Im Allgemeinen macht bereits diese kurze Einführung deutlich, dass die Beschreibung und Implementierung von fortgeschrittenen Parsern mehr Aufwand erfordern würde, sowohl in Bezug auf den Umfang als auch auf die Zeit, die sie für ihre Beherrschung benötigen. In diesem Artikel werden wir uns also auf den einfachsten Parser beschränken — den rekursiven Abstieg.

Planung

Die Aufgabe des Parsers besteht also darin, den seiner Eingabe zugeführten Text zu lesen, ihn in einen Strom unteilbarer Fragmente (Token) zu zerlegen und dann mit den zulässigen Sprachstrukturen zu vergleichen, die mit der MQL-Grammatik in der BNF-Notation oder in einem benachbarten Text beschrieben werden.

Für den Anfang brauchen wir eine Klasse, die Dateien liest — wir werden sie FileReader nennen. Da das MQL-Projekt aus mehreren Dateien bestehen kann, die von der Hauptdatei mit der Direktive #include eingebunden sind, kann es notwendig sein, viele FileReader-Instanzen zu haben, so dass wir eine weitere Klasse, FileReaderController, in unsere Entwicklungen einbeziehen werden.

Im Großen und Ganzen stellen die Texte aus den zu verarbeitenden Dateien eine Standardzeichenkette dar. Allerdings müssen wir sie zwischen verschiedenen Klassen übertragen, während MQL leider keine Zeichenkettenzeiger zulässt (ich erinnere mich an die Referenzen, aber sie können nicht für die Deklaration von Klassenmitgliedern verwendet werden, während die einzige Alternative — die Übergabe der Referenz auf alle Methoden über Eingaben ist schwierig). Daher werden wir eine separate Klasse, Source, erstellen, die einen String-Wrapper darstellt. Sie wird eine weitere wichtige Funktion ausführen.

Es geht darum, dass wir durch die Verknüpfung der 'includes' (und damit durch das rekursive Lesen der Headerdateien aus Abhängigkeiten) einen konsolidierten Text aus allen Dateien der Steuerungsausgabe erhalten. Um Fehler zu erkennen, müssen wir die Verschiebung im konsolidierten Quellcode nutzen, um den Namen und die Zeichenkette der Originaldatei zu erhalten, aus der der Text übernommen wurde. Diese 'Zuordnung' von Quellcodepositionen in Dateien wird auch von der Klasse Source unterstützt und gespeichert.

Die Frage ist hier relevant: War es möglich, die Quellcodes zusammenzuführen, statt jede Datei separat zu verarbeiten? Ja, wahrscheinlich wäre es sogar noch korrekter, aber dann müsste jede Datei eine eigene Instanz des Parsers erstellen und dann die Syntaxbäume, die der Parser bei der Ausgabe generiert, irgendwie zusammenfügen. Daher entschied ich mich, die Quellcodes zu kombinieren und einem einzelnen Parser zu anzubieten. Interessierte können Sie mit einem alternativen Ansatz experimentieren, wenn sie das wünschen.

Damit der FileReaderController die #include-Direktiven finden kann, ist es notwendig, nicht nur den Text aus Dateien zu lesen, sondern auch eine Vorschau bei der Suche nach diesen Direktiven durchzuführen. Daher ist eine Art Präprozessor erforderlich. In MQL macht er eine andere gute Arbeit. Insbesondere erlaubt er, Makros zu identifizieren und sie dann durch tatsächliche Ausdrücke zu ersetzen (außerdem berücksichtigt es die mögliche Rekursion des Aufrufs eines Makros aus einem Makro). Für unserem ersten MQL-Parser wäre es jedoch besser, nicht alle Aufgaben gleichzeitig zu übernehmen. Daher werden wir in unserem Präprozessor keine Makros verarbeiten — das würde erfordern, dass die Grammatik der Makros nicht nur zusätzlich beschrieben, sondern auch während der Ausführung interpretiert werden müssten, um die korrekten Ausdrücke im Quellcode zu ersetzen. Erinnern Sie sich, was wir über den Interpreter in der Einleitung gesagt haben? Nun, hier wäre es nützlich, und es wird später klar werden, warum es wichtig ist. Dies ist der Bereich unserer unabhängigen Experimente Nummer 2.

Der Präprozessor wird in der Klasse Preprocessor implementiert. Auf seiner Ebene findet ein eher kontroverser Prozess statt. Beim Lesen der Dateien und beim Suchen nach #include-Direktiven in ihnen erfolgt das Parsen und Bewegen innerhalb des Textes auf der untersten Ebene — Zeichen für Zeichen. Der Präprozessor übergibt jedoch "transparent" alles, was keine Direktive ist, an sich selbst und arbeitet mit den größten Blöcken am Ausgang — ganze Dateien oder Dateifragmente zwischen Direktiven. Und dann wird das Parsen auf einer mittleren Ebene fortgesetzt, um zu beschreiben, welche, müssen wir ein paar Begriffe einführen.

Zuerst einmal ist es eine lexikalische Einheit, eine abstrakte minimale Einheit der lexikalischen Analyse, eine Teilzeichenkette ungleich Null Länge. Oft wird damit ein anderer Begriff verwendet — der Token. Das ist eine andere Analyseeinheit, die nicht abstrakt, sondern konkret ist. Beide stellen ein Textfragment dar, z.B. ein einzelnes Zeichen, ein Wort oder sogar einen Kommentarblock. Der feine Unterschied zwischen ihnen ist, dass wir Fragmente mit einer Bedeutung auf der Token-Ebene markieren. Wenn beispielsweise das Wort 'int' im Text erscheint, ist es eine lexikalische Einheit für MQL, die wir mit Token INT bezeichnen werden - ein Element in der Aufzählung aller zulässigen Token in der MQL-Sprache. Mit anderen Worten, der Satz lexikalischer Einheiten bedeutet ein Wörterbuch von Zeichenketten, die den Tokentypen entsprechen.

Einer der Vorteile von Token ist, dass sie es ermöglichen, den Text in Fragmente aufzuteilen, die länger als Zeichen sind. Dadurch wird der Text in zwei Durchgängen analysiert: Aus dem Buchstabenstrom werden zunächst High-Level-Token gebildet und darauf aufbauend Sprachstrukturen analysiert. Dies ermöglicht eine erhebliche Vereinfachung der Sprachgrammatik und der Parserbedienung.

Die Sonderklasse Scanner hebt Token im Text hervor. Es kann als ein Low-Level-Parser mit der vordefinierten und "fest verdrahteten" Grammatik betrachtet werden, der den Text durch Buchstaben verarbeitet. Die genauen Arten von Token, die benötigt werden, sind im Folgenden aufgeführt. Wenn jemand sich auf den Weg zum Experiment Nummer 1 macht (Laden jeder Datei in einen dedizierten Parser), dann wird es dort sein, wo wir den Präprozessor mit dem Scanner kombinieren können und, sobald das Token '#include <something>' gefunden wird, einen neuen FileReader, einen neuen Scanner und einen neuen Parser erstellen und die Kontrolle an sie übertragen.

Alle (reservierten) Schlüsselwörter, sowie die Symbole der Zeichensetzung und der Operationen, sind die Token von MQL. Die vollständige Liste der MQL-Schlüsselwörter ist in der Datei reserved.txt angehängt und im Quellcode des Scanners enthalten.

Bezeichner, Zahlen, Zeichenketten, Literale und andere Konstanten, wie z.B. Datumsangaben, werden ebenfalls unabhängige Token sein.

Beim Parsen eines Textes in Token werden alle Leerzeichen, Zeilenumbrüche und Tabellierungen unterdrückt (ignoriert). Die einzige Ausnahme in Form einer speziellen Verarbeitung sollte für Zeilenumbrüche gemacht werden, da deren Zählung es erlaubt, auf eine Zeile hinzuweisen, die einen Fehler enthält, falls vorhanden.

Nachdem wir also die Scanner-Eingabe mit einem konsolidierten Text versorgt haben, erhalten wir eine Liste von Token am Ausgang. Es ist diese Liste von Token, die vom Parser verarbeitet werden, den wir in der Klasse Parser implementieren.

Um Token nach den MQL-Regeln zu interpretieren, ist es notwendig, die Grammatik dem Parser in der BNF-Notation zu übergeben. Um die Grammatik zu beschreiben, versuchen wir, den vom Parser boost::spirit verwendeten Ansatz in einer vereinfachten Form zu wiederholen. Im Wesentlichen sind die Grammatikregeln mit den Ausdrücken der MQL-Ausdrücke zu beschreiben, da einige Operatoren überladen sind.

Zu diesem Zweck stellen wir Ihnen die Hierarchie der Klassen Terminal, NonTerminal und deren Derivate vor. Terminal ist die Basisklasse, die standardmäßig einem Unit-Token entspricht. Wie es im theoretischen Teil gesagt wurde, ist das Terminal ein endliches, unteilbares Element des Parsens der Regeln: Wenn ein Zeichen an der aktuellen Position des Textes gefunden wird, das mit dem Terminal-Token übereinstimmt, bedeutet dies, dass das Zeichen der Grammatik entspricht. Wir können es lesen und weitermachen.

Wir werden Non-Terminale für komplexe Strukturen verwenden, in denen Terminale und andere Non-Terminale in verschiedenen Kombinationen verwendet werden können. Dies kann an einem Beispiel veranschaulicht werden.

Angenommen, wir müssen eine einfache Grammatik beschreiben, um Ausdrücke zu berechnen, in der nur ganze Zahlen und Operationen 'plus' ('+') und 'multiply' ('*') verwendet werden können. Der Einfachheit halber beschränken wir uns auf ein Szenario, in dem es nur zwei Operanden gibt, wie beispielsweise 10+1 oder 5*6.

Basierend auf dieser Aufgabe ist es zunächst notwendig, das Terminal zu identifizieren, das einer ganzen Zahl entspricht. Es ist dieses Terminal, das mit jedem gültigen Operanden im Ausdruck verglichen wird. Da der Scanner jedes Mal, wenn er eine ganze Zahl im Text findet, das entsprechende Token CONST_INTEGER erzeugt, definieren wir das Objekt der Klasse Terminal, das sich auf dieses Token bezieht. In einem Pseudocode wird dies sein:

Terminal value = CONST_INTEGER;

Dieser Eintrag bedeutet, dass wir das Objekt 'value' der Klasse Terminal erstellt haben, das an das Token 'integer' angehängt ist.

Betriebssymbole sind auch Terminale mit den entsprechenden Token PLUS und STAR, die vom Scanner für die Einzelzeichen '+' und '*' erzeugt werden:

Terminal plus = PLUS;
Terminal star = STAR;

Um eine von ihnen im Ausdruck verwenden zu können, stellen wir ein Non-Terminal vor, das zwei Operationen durch OR kombiniert:

NonTerminal operation = plus | star;

Hier kommt das Überladen des Operators ins Spiel: In der Klasse Terminal (und allen ihren Nachkommen) muss operator| Referenzen vom übergeordneten Objekt (in diesem Fall 'operation') auf die Nachkommen ('plus' und 'star') erstellen und mit ihnen die Art der Operation markieren — logisches ODER.

Wenn der Parser anfängt, die Operation Non-Terminal auf Übereinstimmung mit dem Text unter dem Cursor zu überprüfen, delegiert er die weitere Überprüfung ("Tiefe") an das Objekt 'operation', und dieses Objekt ruft in der Schleife die Überprüfung auf nachfolgende Elemente 'plus' und 'star' auf (bis zur ersten Übereinstimmung, da es OR ist). Da es sich um Terminale handelt, geben sie einfach ihre Token an den Parser zurück, und letzterer wird herausfinden, ob das Zeichen im Text mit einer der Operationen übereinstimmt.

Der Ausdruck kann aus mehreren Werten und Operationen unter ihnen bestehen. Daher ist der Ausdruck auch ein Non-Terminal und muss über Terminale und Non-Terminale "erweitert" werden.

NonTerminal expression = value + operation + value;

Hier überladen wir operator '+', d.h. Operanden müssen in der angegebenen Reihenfolge aufeinander folgen. Auch hier bedeutet die Implementierung der Funktion, dass das übergeordnete Non-Terminal 'expression' die Referenzen auf die nachfolgenden Objekte 'value', 'operation' und einen anderen 'value' speichern muss, wobei die Betriebsart logisch UND ist. In diesem Fall ist die Regel nämlich nur dann zu befolgen, wenn alle Komponenten verfügbar sind.

Die Überprüfung des Textes mit dem Parser auf Übereinstimmung mit dem richtigen Ausdruck erfordert zunächst eine Überprüfung im Array der Referenzen 'expression' und dann in den Objekten 'value' und 'operation' (letztere beziehen sich rekursiv auf 'plus' und 'minus') und schließlich wieder auf 'value'. Wenn die Prüfung bis auf die Ebene der Terminale heruntergeht, wird der Wert des Tokens mit dem aktuellen Zeichen im Text verglichen, und wenn sie übereinstimmen, wechselt der Cursor zum nächsten Token; wenn nicht, dann sollte nach einer Alternative gesucht werden. Wenn beispielsweise in diesem Fall die Prüfung auf die Operation 'plus' erfolglos ist, wird die Prüfung auf 'Stern' fortgesetzt. Wenn alle Alternativen ausgeschöpft sind und keine Übereinstimmung gefunden wurde, bedeutet das, dass die Grammatikregeln verletzt werden.

Die Operatoren '|' und '+' sind nicht alle Operatoren, die wir in unseren Klassen überladen werden. Wir werden sie im Abschnitt Implementierung ausführlich beschreiben.

Die Deklaration der Objekte der Klasse Terminal und ihrer Derivate, die die Referenzen auf andere, immer kleinere Dinge enthalten, bildet den abstrakten Syntaxbaum (AST) der vordefinierten Grammatik. Es ist abstrakt, weil es nicht mit den spezifischen Token aus dem Eingabetext verknüpft ist, d.h. theoretisch beschreibt die Grammatik einen unendlichen Satz gültiger Zeichenketten - in unserem Fall MQL-Codes.

Somit haben wir die Hauptklassen des Projekts skizziert. Um die Darstellung des Gesamtbildes zu erleichtern, fassen wir sie in einem UML-Diagramm der Klassen zusammen.


UML-Diagram der Klassen des MQL Parsings

UML-Diagram der Klassen des MQL Parsings

Einige Klassen, wie z.B. TreeNode, wurden noch nicht besprochen. Der Parser verwendet seine Objekte, während er den Eingabetext analysiert, um alle gefundenen Übereinstimmungen 'terminal=token' zu speichern. Als Ergebnis erhalten wir am Ausgang den sogenannten konkreten Syntaxbaum (CST), in dem alle Token irgendwie hierarchisch in die Terminale und Non-Terminale der Grammatik eingebunden sind.

Grundsätzlich ist die Erstellung des Baums optional, da er sich für die realen Quellcodes als zu groß erweisen kann. Anstatt die Parsing-Ausgaben als Baum zu erhalten, stellen wir die Callback-Schnittstelle — Callback — zur Verfügung. Nachdem wir unser Objekt erstellt haben, das diese Schnittstelle implementiert, werden wir es an den Parser übergeben und können Benachrichtigungen über jede produzierte 'production' erhalten, d.h. jede funktionierende Grammatikregel. So können wir Syntax und Semantik während der Auswertung analysieren, ohne auf einen vollständigen Baum zu warten.

Die Klassen von Non-Terminalen mit dem Präfix 'Hidden' werden verwendet, um automatisch implizit die Zwischengruppen von Grammatikobjekten zu erstellen, die wir im nächsten Abschnitt näher beschreiben werden.


Umsetzung

Das Lesen von Dateien

Quelle

Die Klasse Source ist zunächst einmal ein Speicher der Zeichenkette, die den zu verarbeitenden Text enthält. Im Grunde genommen sieht sie wie folgt aus:

#define SOURCE_LENGTH 100000

class Source
{
  private:
    string source;

  public:
    Source(const uint length = SOURCE_LENGTH)
    {
      StringInit(source, length);
    }

    Source *operator+=(const string &x)
    {
      source += x;
      return &this;
    }

    Source *operator+=(const ushort x)
    {
      source += ShortToString(x);
      return &this;
    }
    
    ushort operator[](uint i) const
    {
      return source[i];
    }
    
    string get(uint start = 0, uint length = -1) const
    {
      return StringSubstr(source, start, length);
    }
    
    uint length() const
    {
      return StringLen(source);
    }
};

Die Klasse hat die Variable 'source' für den Text und überschreibt Operatoren für die häufigsten Operationen mit Zeichenketten. Lassen wir die zweite Rolle dieser Klasse bisher hinter den Kulissen, die darin besteht, die Liste der Dateien zu pflegen, aus denen die gespeicherte Zeichenkette zusammengesetzt wird. Wenn wir ein solches 'wrapping' für den Eingabe-Text haben, können wir versuchen, ihn aus einer Datei zu befüllen. Die Klasse FileReader ist für diese Aufgabe verantwortlich.


FileReader

Bevor wir mit der Programmierung beginnen, sollte die Methode zum Öffnen und Lesen der Datei definiert werden. Da wir einen Text bearbeiten, ist es logisch, den Modus FILE_TXT auszuwählen. Dies würde uns von der manuellen Kontrolle über die Zeilenumbruchzeichen befreien, die darüber hinaus in verschiedenen Editoren unterschiedlich kodiert werden können (normalerweise ist es das Symbolpaar CR LF; in öffentlich zugänglichen MQL-Quellcodes können Sie jedoch Alternativen wie nur CR oder nur LF sehen). Es sollte daran erinnert werden, dass Dateien im Textmodus Zeichenkette für Zeichenkette gelesen werden.

Ein weiteres Problem, an das man denken muss, ist die Unterstützung von Texten in verschiedenen Kodierungen. Da wir mehrere verschiedene Dateien lesen werden, von denen ein Teil als Einzelbyte-Strings (ANSI) gespeichert werden kann, während ein anderer Teil als breitere Doppelbyte-Strings (UNICODE), ist es besser, das System unterwegs die richtigen Modi auswählen zu lassen, d.h. von Datei zu Datei. Darüber hinaus können Dateien auch in der UTF-8-Codierung gespeichert werden.

Es stellt sich heraus, dass MQL verschiedene Textdateien automatisch und korrekt lesen kann, wenn die folgenden Eingaben für die Funktion FileOpen eingestellt sind:

FileOpen(filename, FILE_READ | FILE_TXT | FILE_ANSI, 0, CP_UTF8);

Wir werden dann diese Kombination verwenden, nachdem wir ihr standardmäßig die Kennzeichen FILE_SHARE_READ | FILE_SHARE_WRITE hinzugefügt haben.

In der Klasse FileReader stellen wir Mitglieder zur Verfügung, die den Dateinamen ('filename'), den offenen Datei-Deskriptor ('handle') und die aktuelle Textzeile ('line') speichern.

class FileReader
{
  protected:
    const string filename;
    int handle;
    string line;

Zusätzlich verfolgen wir die aktuelle Zeilennummer und die Cursorposition in der Zeile (Spalte).

    int linenumber;
    int cursor;

Wir werden die gelesenen Zeilen in einer Instanz des Objekts Source speichern.

    Source *text;

Wir übergeben den Dateinamen, die Flags und das fertige Quellobjekt für den Empfang der Daten an den Konstruktor.

  public:
    FileReader(const string _filename, Source *container = NULL, const int flags = FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE, const uint codepage = CP_UTF8): filename(_filename)
    {
      handle = FileOpen(filename, flags, 0, codepage);
      if(handle == INVALID_HANDLE)
      {
        Print("FileOpen failed ", _filename, " ", GetLastError());
      }
      line = NULL;
      cursor = 0;
      linenumber = 0;
      text = container;
    }

    string pathname() const
    {
      return filename;
    }

Lassen Sie uns überprüfen, ob die Datei erfolgreich geöffnet wurde und wie man den Deskriptor im Destruktor schließt.

    bool isOK()
    {
      return (handle > 0);
    }
    
    ~FileReader()
    {
      FileClose(handle);
    }

Das zeichenweise Lesen der Daten aus der Datei wird durch die Methode getChar sichergestellt.

    ushort getChar(const bool autonextline = true)
    {
      if(cursor >= StringLen(line))
      {
        if(autonextline)
        {
          if(!scanLine()) return 0;
          cursor = 0;
        }
        else
        {
          return 0;
        }
      }
      return StringGetCharacter(line, cursor++);
    }

Wenn eine Zeile mit der Textzeile leer ist oder bis zum Ende gelesen wird, versucht diese Methode, die nächste Zeile mit der Methode scanLine zu lesen. Wenn die Zeile 'line' einige unverarbeitete Zeichen enthält, gibt getChar einfach das Zeichen unter dem Cursor zurück und bewegt den Cursor dann an die nächste Position.

Die Methode scanLine ist auf naheliegende Weise definiert:

    bool scanLine()
    {
      if(!FileIsEnding(handle))
      {
        line = FileReadString(handle);
        linenumber++;
        cursor = 0;
        if(text != NULL)
        {
          text += line;
          text += '\n';
        }
        return true;
      }
      
      return false;
    }

Beachten Sie, dass wir, da die Datei im Textmodus geöffnet wird, keine Zeilenumbrüche erhalten; wir benötigen sie jedoch, um die Zeilen zu zählen und als letzte Zeichen einiger Sprachstrukturen, wie z.B. einzeilige Kommentare. Also fügen wir das Symbol '\n' hinzu.

Neben dem Lesen der Daten aus der Datei als solche muss die Klasse FileReader den Vergleich der Eingabedaten unter dem Cursor mit lexikalischen Einheiten ermöglichen. Zu diesem Zweck sollten wir die folgenden Methoden hinzufügen.

    bool probe(const string lexeme) const
    {
      return StringFind(line, lexeme, cursor) == cursor;
    }

    bool match(const string lexeme) const
    {
      ushort c = StringGetCharacter(line, cursor + StringLen(lexeme));
      return probe(lexeme) && (c == ' ' || c == '\t' || c == 0);
    }
    
    bool consume(const string lexeme)
    {
      if(match(lexeme))
      {
        advance(StringLen(lexeme));
        return true;
      }
      return false;
    }

    void advance(const int next)
    {
      cursor += next;
      if(cursor > StringLen(line))
      {
        error(StringFormat("line is out of bounds [%d+%d]", cursor, next));
      }
    }

Die Methode 'probe' vergleicht den Text mit der übergebenen lexikalischen Einheit. Die Methode 'match' macht praktisch dasselbe, prüft aber zusätzlich, ob die lexikalische Einheit als ein einzelnes Wort erwähnt wird, d.h. es muss ein Trennzeichen folgen, wie z.B. Leerzeichen, Tabellierung oder Zeilenende. Die Methode 'consume' "verschlingt" die übergebene lexikalische Einheit/Wort, d.h. sie überprüft, ob der eingegebene Text mit dem vordefinierten Text übereinstimmt, und bewegt im Erfolgsfall den Cursor über das Ende der lexikalischen Einheit. Im Fehlerfall wird der Cursor nicht bewegt, während die Methode 'false' zurückgibt. Die Methode 'advance' bewegt den Cursor einfach um eine vorgegebene Anzahl von Zeichen vorwärts.

Abschließend betrachten wir eine kleine Methode, die das Zeichen des Dateiendes zurückgibt.

    bool isEOF()
    {
      return FileIsEnding(handle) && cursor >= StringLen(line);
    }

Es gibt weitere Hilfsmethoden zum Lesen der Felder dieser Klasse, die Sie in den beigefügten Quellcodes finden.

Objekte der Klasse FileReader müssen irgendwo angelegt werden. Lassen Sie uns der Klasse FileReaderController übertragen.

FileReaderController

In der Klasse FileReaderController müssen ein Stapel von eingebundenen Dateien ('includes'), eine Zuordnung von bereits eingebundenen Dateien ('files'), ein Pointer auf die aktuelle Datei ('current') und der Eingabe-Text ('source') gepflegt werden.

class FileReaderController
{
  protected:
    Stack<FileReader *> includes;
    Map<string, FileReader *> files;
    FileReader *current;
    const int flags;
    const uint codepage;
    
    ushort lastChar;
    Source *source;

Listen, Stapel, Arrays, wie z.B. BaseArray, und Zuordnungen ('Map'), die in Quellcodes vorkommen, werden aus unterstützenden Headerdateien aufgenommen, die hier nicht beschrieben werden, da ich sie bereits in meinen vorherigen Artikeln verwendet habe. Natürlich ist jedoch das komplette Archiv beigefügt.

Der Controller erzeugt in seinem Konstruktor ein leeres Objekt 'source':

  public:
    FileReaderController(const int _flags = FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE, const uint _codepage = CP_UTF8, const uint _length = SOURCE_LENGTH): flags(_flags), codepage(_codepage)
    {
      current = NULL;
      lastChar = 0;
      source = new Source(_length);
    }

'source' sowie die untergeordneten Objekte von FileReader werden von der Zuordnung im Destruktor freigegeben:

#define CLEAR(P) if(CheckPointer(P) == POINTER_DYNAMIC) delete P;

    ~FileReaderController()
    {
      for(int i = 0; i < files.getSize(); i++)
      {
        CLEAR(files[i]);
      }
      delete source;
    }

Um die eine oder andere Datei in die Verarbeitung einzubeziehen, einschließlich der allerersten Projektdatei mit der Erweiterung mq5, lassen Sie uns die Methode 'include' vorsehen.

    bool include(const string _filename)
    {
      Print((current != NULL ? "Including " : "Processing "), _filename);
      
      if(files.containsKey(_filename)) return true;
      
      if(current != NULL)
      {
        includes.push(current);
      }
      
      current = new FileReader(_filename, source, flags, codepage);
      source.mark(source.length(), current.pathname());
      
      files.put(_filename, current);
      
      return current.isOK();
    }

Es prüft die Zuordnung 'files', ob die vordefinierte Datei bereits verarbeitet wurde und gibt sofort 'true' zurück, wenn die Datei verfügbar ist. Andernfalls wird der Prozess fortgesetzt. Wenn dies die allererste Datei ist, erstellen wir das Objekt FileReader, machen es 'current' und speichern es in der Zuordnung 'files'. Wenn dies nicht die erste Datei ist, d.h. eine andere Datei in diesem Moment verarbeitet wird, dann sollten wir sie im Stapel 'includes' speichern. Sobald die eingebundene Datei vollständig verarbeitet ist, kehren wir zur Verarbeitung der aktuellen Datei zurück, beginnend mit dem Zeitpunkt, an dem die andere Datei eingebunden wurde.

Eine Zeile in dieser Methode 'include' wird noch nicht übersetzt:

      source.mark(source.length(), current.pathname());

Die Klasse 'source' enthält noch keine Methode 'mark'. Wie aus dem Kontext ersichtlich sein sollte, wechseln wir an dieser Stelle von einer Datei zur anderen, und deshalb sollten wir irgendwo die Quelle und ihre Verschiebung im kombinierten Text markieren. Das ist es, was die Methode 'mark' tun wird. Zu jedem Zeitpunkt ist die aktuelle Länge des Eingabetextes der Punkt, an dem die Daten der neuen Datei hinzugefügt werden. Kehren wir zurück zur Klasse Source und fügen die Zuordnung der Dateien hinzu:

class Source
{
  private:
    Map<uint,string> files;

  public:
    void mark(const uint offset, const string file)
    {
      files.put(offset, file);
    }

Die Hauptaufgabe des Lesens der Zeichen aus der Datei in der Klasse FileReaderController wird mit der Methode getChar ausgeführt, die einen Teil der Arbeit an das aktuelle Objekt FileReader delegiert.

    ushort getChar(const bool autonextline = true)
    {
      if(current == NULL) return 0;
      
      if(!current.isEOF())
      {
        lastChar = current.getChar(autonextline);
        return lastChar;
      }
      else
      {
        while(includes.size() > 0)
        {
          current = includes.pop();
          source.mark(source.length(), current.pathname());
          if(!current.isEOF())
          {
            lastChar = current.getChar();
            return lastChar;
          }
        }
      }
      return 0;
    }

Wenn es eine aktuelle Datei gibt und sie bis zum Ende nicht gelesen wurde, werden wir ihre Methode getChar aufrufen und das erhaltene Zeichen zurückgeben. Wenn die aktuelle Datei bis zum Ende gelesen wurde, dann werden wir prüfen, ob es Anweisungen gibt, um alle anderen Dateien in den Stapel 'includes' aufzunehmen. Wenn es Dateien gibt, extrahieren wir die obere, machen sie 'current' und lesen die Zeichen daraus weiter. Zusätzlich sollten wir uns daran erinnern, im Objekt 'source' darauf hinzuweisen, dass die Datenquelle auf die ältere Datei umgestellt wurde.

Die Klasse FileReaderController kann auch das Vorzeichen des Abschlusses des Lesevorgangs zurückgeben.

    bool isAtEnd()
    {
      return current == NULL || (current.isEOF() && includes.size() == 0);
    }

Lassen Sie uns unter anderem ein paar Methoden zur Verfügung stellen, um die aktuelle Datei und den Text zu erhalten.

    const Source *text() const
    {
      return source;
    }
    
    FileReader *reader()
    {
      return current;
    }

Jetzt ist alles bereit für die Vorverarbeitung der Dateien.


Präprozessor

Der Präprozessor steuert die einzige Instanz der Klasse FileReaderController (Controller) und entscheidet, ob es notwendig ist, Header-Dateien zu laden (Flag 'loadIncludes'):

class Preprocessor
{
  protected:
    FileReaderController *controller;
    const string includes;
    bool loadIncludes;

Es geht darum, dass wir einige Dateien ohne Abhängigkeiten verarbeiten wollen — zum Beispiel zum Zwecke der Fehlersuche oder zur Reduzierung der Laufzeit. Wir werden den Standardordner für Header-Dateien in der Zeichenkettenvariablen 'includes' speichern.

Der Konstruktor erhält alle diese Werte und den Namen der Ausgangsdatei (und den Pfad dazu) vom Benutzer, erstellt einen Controller und ruft die Methode 'include' für die Datei auf.

  public:
    Preprocessor(const string _filename, const string _includes, const bool _loadIncludes = false, const int _flags = FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE, const uint _codepage = CP_UTF8, const uint _length = SOURCE_LENGTH): includes(_includes)
    {
      controller = new FileReaderController(_flags, _codepage, _length);
      controller.include(_filename);
      loadIncludes = _loadIncludes;
    }

Nun schreiben wir die Methode 'run', die vom Client direkt aufgerufen wird, um die Verarbeitung einer oder mehrerer Dateien zu starten.

    bool run()
    {
      while(!controller.isAtEnd())
      {
        if(!scanLexeme()) return false;
      }
      return true;
    }

Wir werden lexikalische Einheiten lesen, bis die Steuerung gegen das Ende der Daten stößt.

Hier kommt die Methode 'scanLexeme':

    bool scanLexeme()
    {
      ushort c = controller.getChar();
      
      switch(c)
      {
        case '#':
          if(controller.reader().consume("include"))
          {
            if(!include())
            {
              controller.reader().error("bad include");
              return false;
            }
          }
          break;
          ...
      }
      return true; // Symbol verspeist
    }

Wenn das Programm das Zeichen '#' sieht, dann versucht es, das nächste Wort 'include' "aufzunehmen". Wenn es nicht vorhanden ist, dann war es ein einzelnes Zeichen '#', das einfach übersprungen werden konnte (getChar bewegt den Cursor eine Position weiter). Wird das Wort 'include' gefunden, muss die Direktive verarbeitet werden, was mit der Methode 'include' geschieht.

    bool include()
    {
      ushort c = skipWhitespace();
      
      if(c == '"' || c == '<')
      {
        ushort q = c;
        if(q == '<') q = '>';
        
        int start = controller.reader().column();
        
        do
        {
          c = controller.getChar();
        }
        while(c != q && c != 0);
        
        if(c == q)
        {
          if(loadIncludes)
          {
            Print(controller.reader().source());

            int stop = controller.reader().column();
  
            string name = StringSubstr(controller.reader().source(), start, stop - start - 1);
            string path = "";
  
            if(q == '"')
            {
              path = controller.reader().pathname();
              StringReplace(path, "\\", "/");
              string parts[];
              int n = StringSplit(path, '/', parts);
              if(n > 0)
              {
                ArrayResize(parts, n - 1);
              }
              else
              {
                Print("Path is empty: ", path);
                return false;
              }
              
              int upfolder = 0;
              while(StringFind(name, "../") == 0)
              {
                name = StringSubstr(name, 3);
                upfolder++;
              }
              
              if(upfolder > 0 && upfolder < ArraySize(parts))
              {
                ArrayResize(parts, ArraySize(parts) - upfolder);
              }
              
              path = StringImplodeExt(parts, CharToString('/')) + "/";
            }
            else // '<' '>'
            {
              path = includes; // folder;
            }
            
            return controller.include(path + name);
          }
          else
          {
            return true;
          }
        }
        else
        {
          Print("Incomplete include");
        }
      }
      return false;
    }

Diese Methode überspringt alle Leerzeichen nach dem Wort 'include' mit 'skipWhitespace' (wird hier nicht berücksichtigt), sucht das Eröffnungszitat oder das Zeichen '<', durchsucht den Text bis zum doppelten Anführungszeichen oder Abschlusszeichen '>' und wählt schließlich die Zeichenfolge mit Pfad und Namen aus Header-Datei. Als Nächstes werden die Verarbeitungsoptionen für das Hochladen einer Datei aus demselben Ordner oder aus dem Standardordner der Header-Dateien angezeigt. Als Ergebnis wird ein neuer Pfad und ein neuer Name zum Laden gebildet, woraufhin der Controller angewiesen wird, die Datei zu verarbeiten.

Zusammen mit der Verarbeitung der Direktive #include müssen wir Kommentarblöcke und Zeichenketten überspringen, um sie nicht als Anweisungen zu interpretieren, wenn die lexikalische Einheit '#include' darin enthalten sein soll. Fügen wir daher die entsprechenden Optionen in den Operator 'switch' in der Methode 'scanLexeme' ein.

        case '/':
          if(controller.reader().probe("*"))
          {
            controller.reader().advance(1);
            if(!blockcomment())
            {
              controller.reader().error("bad block comment");
              return false;
            }
          }
          else
          if(controller.reader().probe("/"))
          {
            controller.reader().advance(1);
            linecomment();
          }
          break;
        case '"':
          if(!literal())
          {
            controller.reader().error("unterminated string");
            return false;
          }
          break;

So werden z.B. Kommentarblöcke übersprungen:

    bool blockcomment()
    {
      ushort c = 0, c_;
      
      do
      {
        c_ = c;
        c = controller.getChar();
        if(c == '/' && c_ == '*') return true;
      }
      while(!controller.reader().isEOF());
      
      return false;
    }

Andere Hilfsmethoden sind ähnlich implementiert.

So können wir mit der Klasse 'Preprocessor' und anderen Klassen theoretisch bereits den Text aus Arbeitsdateien laden.

#property script_show_inputs

input string SourceFile = "filename.txt";
input string IncludesFolder = "";
input bool LoadIncludes = false;

void OnStart()
{
  Preprocessor loader(SourceFile, IncludesFolder, LoadIncludes);
  
  if(!loader.run())
  {
    Print("Loader failed");
    return;
  }

  // Ausgabe aller Daten aus einem oder mehrerer Dateien
  int handle = FileOpen("dump.txt", FILE_WRITE | FILE_TXT | FILE_ANSI, 0, CP_UTF8);
  FileWriteString(handle, loader.text().get());
  FileClose(handle);
}

Warum "theoretisch"? Es geht darum, dass MetaTrader nur mit Dateien aus dem "Sandkasten", d.h. dem Verzeichnis MQL5/Files, arbeiten kann. Unser Ziel ist es jedoch, Quellcodes zu verarbeiten, die sich in den Ordnern MQL5/Include, MQL5/Scripts, MQL5/Experts und MQL5/Indicators befinden.

Um diese Einschränkung zu umgehen, verwenden wir die Funktion von Windows, um symbolische Links Dateisystemobjekten zuzuordnen. In unserem Fall eignen sich die sogenannten 'junctions' am besten, um den Zugriff auf Ordner auf dem lokalen Computer weiterzuleiten. Sie werden mit dem Befehl erstellt:

mklink /J neuer_Name vorhandenes_Ziel

Der Parameter neuer_Name ist der Name des neuen virtuellen 'folder', der auf einen realen Ordner existing_target zeigt.

Um Verbindungen zu den angegebenen Ordnern mit Quellcodes zu erstellen, öffnen wir den Ordner MQL5/Files, erstellen Unterordner Sources darin und gehen zu diesem Unterordner. Dann werden wir die angehängte Datei makelink.bat kopieren. Dieses Befehlsskript enthält tatsächlich eine Zeile:

mklink /J %1 "..\..\%1\"

Es wird eine Eingabe %1 benötigt — der Name des Anwendungsordners aus denjenigen innerhalb von MQL5, wie z.B. 'Include'. Der relative Pfad '.\...\..\' legt nahe, dass sich die Befehlsdatei im obigen Ordner MQL5/Files/Sources befindet, und dann wird der Zielordner (vorhandenes_Ziel) als MQL5/%1 gebildet. Wenn wir uns beispielsweise im Ordner Sources befinden, führen wir den Befehl

makelink Include

dann erscheint im Ordner Sources ein virtueller Ordner Include, über den wir in MQL5/Include gelangen. Ebenso können wir "Zwillinge" für Ordner Experts, Scripts, etc. erstellen. Im unteren Bild ist der Explorer mit dem geöffneten Ordner MQL5/Include/Expert mit Standard-Kopfdateien im Ordner MQL5/Files/Sources dargestellt.

Windows Symbolische Verweise für Ordner der MQL5-Quellcodes

Windows Symbolische Verweise für Ordner der MQL5-Quellcodes

Bei Bedarf können wir symbolische Links als normale Dateien löschen (aber natürlich sollten wir zuerst sicherstellen, dass wir den Ordner mit einem kleinen Pfeil in der linken unteren Ecke löschen, nicht den ursprünglichen Ordner).

Wir könnten einen Knoten direkt auf dem Arbeitsordner von Root von MQL5 erstellen, aber ich bevorzuge und empfehle, den Zugriff gelegentlich zu öffnen: Alle MQL-Programme können den Link verwenden, um Ihre Quellcodes zu lesen, einschließlich Logins, Passwörter und streng geheime Handelssysteme, vorausgesetzt, sie sind dort gespeichert.
Beim Erstellen der Links funktioniert der Parameter 'IncludesFolder' des obigen Skripts wirklich: Der Wert von Sources/Include/pointiert auf den realen Ordner MQL5/Include. Im Parameter 'SourceFile' können wir die Analyse anhand des Quellcodes eines Skripts, wie z.B. Sources/Scripts/test.mq5. veranschaulichen.

Tokenisierung

Token-Typen, die im MQL zu unterscheiden sind, werden in der Aufzählung 'TokenType' in der homonymen Headerdatei (Anhang) zusammengefasst. Wir werden es in diesem Artikel nicht darlegen. Beachten wir nur, dass es unter ihnen einstellige Token gibt, wie z.B. verschiedene Klammern und geschweifte Klammern ('(', '[' oder '{'), Gleichheitszeichen '=', plus '+' oder minus '-', sowie zweistellige Token, wie '==', '!=' etc. Separate Token sind außerdem Zahlen, Zeichenketten, Datumsangaben (d.h. Konstanten der unterstützten Typen), alle in MQL reservierten Wörter, wie Operatoren, Typen, dies, Modifikatoren wie 'input', 'const', etc. sowie Identifikatoren (andere Wörter). Zusätzlich gibt es ein Token EOF, das das Ende der Eingabedaten bezeichnet.


Token

Beim Betrachten eines Textes identifiziert der Scanner den Typ jedes nachfolgenden Token durch einen speziellen Algorithmus (siehe unten) und erstellt ein Objekt der Klasse Token. Dies ist eine sehr einfache Klasse.

class Token
{
  private:
    TokenType type;
    int line;
    int offset;
    int length;

  public:
    Token(const TokenType _type, const int _line, const int _offset, const int _length = 0)
    {
      type = _type;
      line = _line;
      offset = _offset;
      length = _length;
    }
    
    TokenType getType() const
    {
      return type;
    }
    
    int getLine() const
    {
      return line;
    }
    ...

    string content(const Source *source) const
    {
      return source.get(offset, length);
    }
};

Das Objekt speichert den Typ des Token, seine Verschiebung innerhalb des Textes und seine Länge. Wenn Sie die Zeichenfolge des Tokens abrufen müssen, übergeben wir einen Zeiger auf die Zeichenfolge 'source' an die Methode 'content' und schneiden das entsprechende Fragment daraus heraus.

Nun ist es an der Zeit, sich auf den Scanner zu beziehen, der auch "Tokenizer" genannt wird.


Scanner (Tokenizer)

In der Klasse Scanner werden wir ein statisches Array mit MQL-Schlüsselwörtern beschreiben:

class Scanner
{
  private:
    static string reserved[];

und dann initialisieren wir es im Quellcode durch das Einbinden der Textdatei

static string Scanner::reserved[] =
{
#include "reserved.txt"
};

Lassen Sie uns zu diesem Array eine statische Zuordnung der Übereinstimmung zwischen der Zeichenkettendarstellung und dem Typ jedes Tokens hinzufügen.

    static Map<string, TokenType> keywords;

Lassen Sie uns die Karte im Konstruktor ausfüllen (siehe unten).

Im Scanner benötigen wir auch einen Zeiger auf Eingaben, die resultierende Liste der Token und mehrere Zähler.

    const Source *source; // wrapped Zeichenkette
    List<Token *> *tokens;
    int start;
    int current;
    int line;

Die Variable 'start' steht immer am Anfang des nächsten zu verarbeitenden Tokens. Die Variable 'current' ist der Cursor, um sich innerhalb des Textes zu bewegen. Sie wird immer von Anfang an vorwärts laufen, da die aktuellen Zeichen auf Übereinstimmung mit einem Token überprüft werden und, sobald eine Übereinstimmung gefunden wird, fällt die Teilzeichenkette von Anfang bis Ende in das neue Token. Die Variable 'line' ist die Nummer der aktuellen Zeile im Gesamttext.

Konstrukteur der Klasse Scanner:

  public:
    Scanner(const Source *_source): line(0), current(0)
    {
      tokens = new List<Token *>();
      if(keywords.getSize() == 0)
      {
        for(int i = 0; i < ArraySize(reserved); i++)
        {
          keywords.put(reserved[i], TokenType(BREAK + i));
        }
      }
      source = _source;
    }

Hier ist BREAK der Token-Typ-Identifikator für das erste reservierte Wort in alphabetischer Reihenfolge. Die Reihenfolge der Zeichenketten in der Datei reserved.txt und der Identifikatoren in der Aufzählung TokenType muss übereinstimmen. So entspricht beispielsweise das Element BREAK in der Enumeration offenbar dem Element 'break'.

Die Methode 'scanTokens' nimmt den zentralen Platz in der Klasse ein.

    List<Token *> *scanTokens()
    {
      while(!isAtEnd())
      {
        // Wir sind am Anfang des nächsten Lexems
        start = current;
        scanToken();
      }
  
      start = current;
      addToken(EOF);
      return tokens;
    }

In seinem Zyklus werden immer mehr neue Token generiert. Die Methoden 'isAtEnd' und 'addToken' sind einfach:

    bool isAtEnd() const
    {
      return (uint)current >= source.length();
    }

    void addToken(TokenType type)
    {
      tokens.add(new Token(type, line, start, current - start));
    }

Die ganze harte Arbeit wird mit der Methode 'scanToken' erledigt. Bevor wir es jedoch vorstellen, sollten wir einige einfache Hilfsmethoden kennenlernen — sie ähneln denen, die wir bereits in der Klasse 'Preprocessor' gesehen haben, deshalb scheint ihr Zweck keine Erklärungen zu benötigen.

    bool match(ushort expected)
    {
      if(isAtEnd()) return false;
      if(source[current] != expected) return false;
  
      current++;
      return true;
    }
    
    ushort previous() const
    {
      if(current > 0) return source[current - 1];
      return 0;
    }
    
    ushort peek() const
    {
      if(isAtEnd()) return '\0';
      return source[current];
    }
    
    ushort peekNext() const
    {
      if((uint)(current + 1) >= source.length()) return '\0';
      return source[current + 1];
    }

    ushort advance()
    {
      current++;
      return source[current - 1];
    }

Nun zurück zur Methode 'scanToken'.

    void scanToken()
    {
      ushort c = advance();
      switch(c)
      {
        case '(': addToken(LEFT_PAREN); break;
        case ')': addToken(RIGHT_PAREN); break;
        ...

Sie liest das nächste Zeichen und erstellt je nach Code ein Token. Wir werden hier nicht alle einstelligen Token anbieten, da sie ähnlich erstellt werden.

Wenn ein Token vorgibt, ein zweistelliger zu sein, wird die Verarbeitung komplizierter:

        case '-': addToken(match('-') ? DEC : (match('=') ? MINUS_EQUAL : MINUS)); break;
        case '+': addToken(match('+') ? INC : (match('=') ? PLUS_EQUAL : PLUS)); break;
        ...

Sich bildende Token für die Lexeme '--', '-=', '++', und '+=' sind unten aufgeführt.

Die aktuelle Version des Scanners ignoriert Kommentare:

        case '/':
          if(match('/'))
          {
            // Ein Kommentar geht bis zum Zeilenende
            while(peek() != '\n' && !isAtEnd()) advance();
          }

Sie können sie, wenn Sie wollen, in speziellen Token speichern.

Blockstrukturen wie Zeichenketten, Literale und Präprozessordirektiven werden in den zugeordneten Hilfsmethoden verarbeitet, wir werden sie nicht im Detail berücksichtigen:

        case '"': _string(); break;
        case '\'': literal(); break;
        case '#': preprocessor(); break;

Hier ist ein Beispiel, wie die Zeichenkette gescannt wird:

    void _string()
    {
      while(!(peek() == '"' && previous() != '\\') && !isAtEnd())
      {
        if(peek() == '\n')
        {
          line++;
        }
        advance();
      }
  
      if(isAtEnd())
      {
        error("Unterminated string");
        return;
      }
  
      advance(); // Das Ende
  
      addToken(CONST_STRING);
    }

Wenn kein Token-Typ erkannt wurde, wird ein Standardtest durchgeführt, bei dem Zahlen, Identifikatoren und Schlüsselwörter überprüft werden.

        default:
        
          if(isDigit(c))
          {
            number();
          }
          else if(isAlpha(c))
          {
            identifier();
          }
          else
          {
            error("Unexpected character `" + ShortToString(c) + "` 0x" + StringFormat("%X", c) + " @ " + (string)current + ":" + source.get(MathMax(current - 10, 0), 20));
          }
          break;

Implementierungen von isDigit und isAlpha sind offensichtlich. Hier wird nur die Methode 'identifier' gezeigt.

    void identifier()
    {
      while(isAlphaNumeric(peek())) advance();

      // Nachschauen, ob das ein reserviertes Wort ist
      string text = source.get(start, current - start);
  
      TokenType type = keywords.get(text);
      if(type == null) type = IDENTIFIER;
      
      addToken(type);
    }

Die vollständige Implementierungen aller Methoden finden Sie in den beigefügten Quellcodes. Um das Rad nicht neu zu erfinden, habe ich einen Teil des Codes aus dem Buch Crafting Interpreters übernommen und natürlich einige Anpassungen vorgenommen.

Im Grunde genommen ist das der ganze Scanner. Wenn keine Fehler auftreten, gibt die Methode 'scanTokens' dem Benutzer eine Liste von Token zurück, die an den Parser übergeben werden können. Der Parser muss jedoch eine Grammatik haben, auf die er sich beim Parsen der Liste der Token beziehen muss. Bevor wir also mit dem Parser fortfahren, müssen wir die Beschreibung der Grammatik besprechen. Wir bilden sie aus den Objekten der Klasse Terminal und deren Derivate.

Beschreibung der Grammatik

Stellen wir uns zunächst vor, dass wir nicht die MQL-Grammatik beschreiben müssen, sondern die einer bestimmten einfachen Sprache zur Berechnung arithmetischer Ausdrücke, d.h. eines Taschenrechners. Dies ist die zulässige Berechnungsformel:

(10 + 1) * 2

Lassen wir nur ganze Zahlen und Operationen '+', '-', '-', '*' und '/' zu, ohne zu priorisieren: Wir werden '(' und ')' für die Priorisierung verwenden.

Der Einstiegspunkt der Grammatik muss ein Non-Terminal sein, das den gesamten Ausdruck beschreibt. Angenommen, es genügt zu schreiben:

NonTerminal expression;

Der Ausdruck besteht aus Operanden, d.h. ganzzahligen Werten und Operatorsymbole. Alle oben genannten Punkte sind Terminale, d.h. sie können basierend auf Token erstellt werden, die vom Scanner unterstützt werden.

Angenommen, wir beschreiben sie wie folgt:

Terminal plus(PLUS), star(STAR), minus(MINUS), slash(SLASH);
Terminal value(CONST_INTEGER);

Wie wir sehen können, muss der Konstrukteur der Terminale die Übergabe des Token-Typs als Parameter ermöglichen.

Der einfachste Ausdruck ist nur eine Zahl. Es wäre logisch, dies wie folgt zu bezeichnen:

expression = value;

Dadurch wird der Zuweisungsoperator neu gestartet. Darin müssen wir die Verknüpfung zum Objekt 'value' (nennen wir es 'eq' von 'equivalence') in einer Variablen innerhalb des 'expression' speichern. Sobald dem Parser zugewiesen wird, den 'Ausdruck' auf Übereinstimmung mit der eingegebenen Zeichenkette zu überprüfen, delegiert er die Überprüfung an das Non-Terminal. Letzteres "sieht" den Link zu 'value' und fordert den Parser auf, den 'value' zu überprüfen, und die Überprüfung erreicht schließlich das Terminal, in dem genau der gleiche Token stattfindet, d.h. der Token im Terminal und der im Eingangsstrom.

Der Ausdruck kann jedoch zusätzlich eine Operation und den zweiten Operanden aufweisen; daher ist es notwendig, die Regel 'expression' zu erweitern. Zu diesem Zweck werden wir das neue Non-Terminal vorläufig beschreiben:

NonTerminal operation;
operation = (plus | star | minus | slash) + value;

Hier finden viele interessante Dinge hinter den Kulissen statt. Der Operator '|' muss in der Klasse überladen werden, um eine logische Gruppierung der Elemente nach OR zu gewährleisten. Der Operator wird jedoch für ein Terminal, d.h. einem einfachen Zeichen, aufgerufen, während wir eine Gruppe von Elementen benötigen. Daher muss das erste Element der Gruppe, für das die Ausführungsumgebung einen Operator aufruft ('plus', in diesem Fall), prüfen, ob es Mitglied einer Gruppe ist und, wenn es noch keine Gruppe gibt, es dynamisch als Objekt der Klasse HiddenNonTerminalOR anlegen. Dann muss die Implementierung des überladenen Operators 'this' und das angrenzende rechte Terminal 'star' (als Argument an den Operator übergeben) zur neu erstellten Gruppe hinzufügen. Der Operator gibt einen Link zu dieser Gruppe zurück, damit die nachfolgenden (verketteten) Operatoren '|' nun für HiddenNonTerminalOR aufgerufen werden können.

Um das Array mit den Gruppenmitgliedern zu pflegen, werden wir sicherlich das Array 'next' in der Klasse bereitstellen. Sein Name bedeutet die nächste Stufe der Detaillierung der Grammatik-Elemente. Für jedes Element, das wir zu diesem Array von Unterknoten hinzufügen, sollten wir einen Backlink zum übergeordneten Knoten setzen. Wir werden es 'parent' nennen. Die Verfügbarkeit von einem 'parent' ungleich Null bedeutet genau die Zugehörigkeit zur Gruppe. Aus der Ausführung des Codes in Klammern ergibt sich, dass wir HiddenNonTerminalOR mit einem Array erhalten, das alle 4 Betriebssymbole enthält.

Dann kommt der überladene Operator '+' ins Spiel. Er muss ähnlich wie der Operator '|' funktionieren, d.h. auch eine implizite Gruppe von Elementen erstellen; diesmal jedoch die der Klasse HiddenNonTerminalAND; und sie müssen nach der Regel logisch UND beim Parsen überprüft werden.

Bitte beachten Sie, dass wir eine Abhängigkeitshierarchie von Terminalen und Nicht-Terminalen gebildet haben — in diesem Fall wird das Objekt HiddenNonTerminalAND zwei untergeordnete Elemente enthalten: Die neu erstellte Gruppe HiddenNonTerminalOR und 'value'. HiddenNonTerminalAND ist wiederum abhängig vom Non-Terminal 'operation'.

Die Priorität der Operationen '|' und '+' besteht darin, dass in Abwesenheit von Klammern zuerst AND und danach OR verarbeitet wird. Das ist genau der Grund, warum wir alle Versionen von Zeichen in 'operation' in Klammern setzen mussten.

Mit der Beschreibung des Non-Terminals 'operation' können wir die Grammatik des Ausdrucks korrigieren:

expression = value + operation;

Sie beschreiben mutmaßlich die Ausdrücke, die als A @ B dargestellt werden, wobei A und B ganze Zahlen sind, während @ eine Aktion ist. Aber es gibt hier einen Knackpunkt.

Wir haben bereits zwei Regeln, die das Objekt 'value' betreffen. Das bedeutet, dass der Link zu einem übergeordneten Satz in der ersten Regel in der zweiten Regel neu geschrieben wird. Damit das nicht passiert, müssen keine Objekte in die Regeln eingefügt werden, sondern deren Kopien.

Zu diesem Zweck werden wir für zwei Operatoren eine Überladung schreiben: '~' und '^'. Der erste von ihnen, der unär, steht vor dem Operanden. In dem Objekt, das den Aufruf der entsprechenden Operatorfunktion erhalten hat, erstellen wir dynamisch ein Proxy-Objekt und geben es an den Aufrufcode zurück. Der zweite Operator ist binär. Zusammen mit dem Objekt übergeben wir ihm die aktuelle Zeichenkettennummer im Grammatik-Quellcode, d.h. die Konstante __LINE__, die vom MQL-Compiler vorgegeben ist. Auf diese Weise werden wir in der Lage sein, die implizit definierten Objektinstanzen durch die Anzahl der Zeilen, in denen sie erzeugt werden, zu unterscheiden. Dies wird helfen, komplexe Grammatiken zu debuggen. Anders ausgedrückt, die Operatoren '~' und '^' verrichten die gleiche Arbeit, aber der erste befindet sich im Freigabemodus, während der zweite im Debugging-Modus.

Die Proxy-Instanz repräsentiert ein Objekt der Klasse HiddenNonTerminal, in dem sich die obige Variable 'eq' auf das Originalobjekt bezieht.

Daher werden wir die Grammatik der Ausdrücke neu schreiben und in Betracht ziehen, Proxy-Objekte zu erstellen.

operation = (plus | star | minus | slash) + ~value;
expression = ~value + operation;

Da 'operation' nur einmal verwendet wird, brauchen wir keine Kopie dafür zu machen. Jede logische Referenz erhöht die Rekursion um eins, wenn sie den Ausdruck erweitert. Um jedoch Fehler in großen Grammatiken zu vermeiden, empfehlen wir, überall Verweise zu machen. Wenn auch nur einmal ein Non-Terminal verwendet wird, kann es später in einem anderen Teil der Grammatik vorkommen. Wir werden den Quellcode mit der Überprüfung auf die Übersteuerung des übergeordneten Knotens zur Verfügung stellen, um eine Fehlermeldung anzuzeigen.

Nun kann unsere Grammatik '10+1' verarbeiten. Aber sie hat die Fähigkeit verloren, eine separate Zahl zu lesen. Tatsächlich muss das Non-Terminal 'operation' optional sein. Lassen Sie uns zu diesem Zweck einen überladenen Operator '*' implementieren. Wenn ein Grammatik-Element mit 0 multipliziert wird, dann können wir es bei der Durchführung der Prüfungen weglassen, da seine Abwesenheit nicht zu einem Fehler führt.

expression = ~value + operation*0;

Die Überladung des Operators der Multiplikation wird es uns ermöglichen, eine weitere wichtige Sache zu implementieren — das Element so oft zu wiederholen, wie wir wollen. In diesem Fall multiplizieren wir das Element mit 1. In der Terminalklasse wird diese Eigenschaft, d.h. Multiplizität oder Optionalität, in der Variablen 'mult' gespeichert. Fälle, in denen ein Element sowohl optional ist als auch mehrfach wiederholt werden kann, lassen sich leicht über zwei Links realisieren: Die Erste muss optional (optional*0) und die Andere mehrfach wiederholt (optional = Element*1) sein.

Die aktuelle Grammatik des Rechners hat noch eine weitere Schwäche. Es ist nicht geeignet für lange Ausdrücke mit mehreren Operationen, wie z.B. 1+2+3+3+4+5. Um dies zu korrigieren, sollten wir das Non-Terminal 'operation' ändern.

operation = (plus | star | minus | slash) + ~expression;

Wir werden 'value' durch den 'expression' selbst ersetzen, nachdem wir damit das zyklische Parsen von immer mehr neuen Enden von Ausdrücken ermöglicht haben.

Der letzte Schliff ist die Unterstützung von Ausdrücken, die von Klammern umschlossen sind. Es ist nicht schwer zu erraten, dass sie die gleiche Rolle spielen wie der Anteilswert ('value'). Deshalb sollten wir sie als Alternative zu zwei Optionen neu definieren: Eine ganze Zahl und ein Unterausdruck in Klammern. Die gesamte Grammatik wird wie folgt angezeigt:

NonTerminal expression;
NonTerminal value;
NonTerminal operation;

Terminal number(CONST_INTEGER);
Terminal left(LEFT_PAREN);
Terminal right(RIGHT_PAREN);
Terminal plus(PLUS), star(STAR), minus(MINUS), slash(SLASH);

value = number | left + expression^__LINE__ + right;
operation = (plus | star | minus | slash) + expression^__LINE__;
expression = value + operation*0;

Lassen Sie uns einen genaueren Blick darauf werfen, wie die oben genannten Klassen intern organisiert sind.


Terminal

In der Klasse Terminal beschreiben wir die Felder für den Tokentyp ('me'), Multiplizitätseigenschaften ('mult'), optionalen Namen ('name', um die Non-Terminale in den Protokollen zu identifizieren), Links zur Produktion ('eq'), zum übergeordneten ('parent') und untergeordneten Elementen (Array 'next').

class Terminal
{
  protected:
    TokenType me;
    int mult;
    string name;
    Terminal *eq;
    BaseArray<Terminal *> next;
    Terminal *parent;

Die Felder werden in Konstruktoren und Setter-Methoden befüllt und mit Getter-Methoden ausgelesen, die wir hier aus Gründen der Kürze nicht diskutieren.

Wir werden die Operatoren nach folgendem Prinzip überladen:

    virtual Terminal *operator|(Terminal &t)
    {
      Terminal *p = &t;
      if(dynamic_cast<HiddenNonTerminalOR *>(p.parent) != NULL)
      {
        p = p.parent;
      }

      if(dynamic_cast<HiddenNonTerminalOR *>(parent) != NULL)
      {
        parent.next << p;
        p.setParent(parent);
      }
      else
      {
        if(parent != NULL)
        {
          Print("Bad OR parent: ", parent.toString(), " in ", toString());

          ... error
        }
        else
        {
          parent = new HiddenNonTerminalOR("hiddenOR");

          p.setParent(parent);
          parent.next << &this;
          parent.next << p;
        }
      }
      return parent;
    }

Hier wird die Gruppierung nach OR dargestellt. Bei AND ist alles ähnlich.

Die Einstellung der Eigenschaft der Multiplizität erfolgt im Operator '*':

    virtual Terminal *operator*(const int times)
    {
      mult = times;
      return &this;
    }

Im Destruktor werden wir uns darum kümmern, die erstellten Instanzen korrekt zu löschen.

    ~Terminal()
    {
      Terminal *p = dynamic_cast<HiddenNonTerminal *>(parent);
      while(CheckPointer(p) != POINTER_INVALID)
      {
        Terminal *d = p;
        if(CheckPointer(p.parent) == POINTER_DYNAMIC)
        {
          p = dynamic_cast<HiddenNonTerminal *>(p.parent);
        }
        else
        {
          p = NULL;
        }
        CLEAR(d);
      }
    }

Zum Schluss die Hauptmethode der Klasse Terminal, die für das Parsen verantwortlich ist.

    virtual bool parse(Parser *parser)
    {
      Token *token = parser.getToken();

      bool eqResult = true;

Hier haben wir die Referenz auf den Parser erhalten und lesen den aktuellen Token daraus (Parserklasse wird unten besprochen).

      if(token.getType() == EOF && mult == 0) return true;

Wenn das Token EOF ist und das aktuelle Element optional ist, bedeutet dies, dass ein korrektes Ende des Textes gefunden wurde.

Dann werden wir prüfen, ob es einen Verweis von überladenem Operator '=' auf die Originalinstanz des Elements gibt, wenn wir uns in der Kopie befinden. Wenn es eine Referenz gibt, senden wir sie zur Überprüfung der Methode 'match' des Parsers.

      if(eq != NULL) // weitersenden
      {
        eqResult = parser.match(eq);
        
        bool lastResult = eqResult;
        
        // Wenn mehrere Tokens erwartet werden und weitere Tokens bereits erfolgreich verarbeitet wurden
        while(eqResult && eq.mult == 1 && parser.getToken() != token && parser.getToken().getType() != EOF)
        {
          token = parser.getToken();
          eqResult = parser.match(eq);
        }
        
        eqResult = lastResult || (mult == 0);
        
        return eqResult; // wurde weitergeleitet
      }

Außerdem wird hier die Situation verarbeitet, in der sich ein Element wiederholen kann ('mult' = 1): Der Parser wird immer wieder aufgerufen, während die Methode 'match' den Erfolg zurückgibt.

Das Erfolgszeichen — 'true' oder 'false' — wird von der Methode 'parse' sowohl in diesem Zweig als auch in anderen Situationen, z.B. für ein Terminal, zurückgegeben:

      if(token.getType() == me) // der Token passt
      {
        parser.advance(parent);
        return true;
      }

Für das Terminal vergleichen wir einfach sein Token 'me' mit dem aktuellen Token in den Eingängen und weisen dem Parser zu, dass er den Cursor mit der Methode 'advance' auf das nächste Input-Token bewegt. Auf die gleiche Weise benachrichtigt der Parser das Client-Programm, dass das Ergebnis im Non-Terminal 'parent' erzeugt wurde.

Für eine Gruppe von Elementen ist alles etwas komplizierter. Betrachten wir logisches AND; die Version für OR wird ähnlich sein. Mit der virtuellen Methode hasAnd (in der Klasse Terminal gibt sie 'false' zurück, während sie in den Nachkommen überschrieben wird) finden wir heraus, ob das Array mit den untergeordneten Elementen zur Überprüfung durch AND gefüllt wurde.

      else
      if(hasAnd()) // prüfen der AND-Bedingung
      {
        parser.pushState();
        for(int i = 0; i < getNext().size(); i++)
        {
          if(!parser.match(getNext()[i]))
          {
            if(mult == 0)
            {
              parser.popState();
              return true;
            }
            else
            {
              parser.popState();
              return false;
            }
          }
        }

        parser.commitState(parent);
        return true;
      }

Da dieses Non-Terminal als korrekt angesehen wird, wenn alle seine Komponenten mit der Grammatik übereinstimmen, werden wir die Methode 'match' des Parsers für alle im Zyklus aufrufen. Wenn mindestens ein negatives Ergebnis vorliegt, schlägt die gesamte Prüfung fehl. Es gibt jedoch eine Ausnahme: Wenn Non-Terminal optional ist, werden die Grammatikregeln weiterhin eingehalten, auch wenn 'false' von der Methode 'match' zurückgegeben wird.

Beachten Sie, dass wir den aktuellen Zustand (pushState) vor dem Zyklus im Parser speichern, ihn beim frühen Beenden wiederherstellen (popState) und im Falle einer vollständig erfolgreichen Prüfung den neuen Zustand (commitState) bestätigen. Dies ist notwendig, um die Benachrichtigungen für den Kundencode auf der neuen 'production' zu verzögern, bis die gesamte Grammatikregel vollständig funktioniert. Das Wort "state" bedeutet einfach die aktuelle Cursorposition im Strom der Input-Token.

Wenn weder Token noch Gruppen von untergeordneten Elementen innerhalb der Methode 'parse' ausgelöst wurden, bleibt nur noch die Überprüfung der Optionalität des aktuellen Objekts:

      else
      if(mult == 0) // letzte Chance
      {
        // parser.advance(); - keine Verarbeitung des Tokens und weiter zum nächsten Ergebnis
        return true;
      }

Andernfalls "fallen" wir an das Methodenende, das einen Fehler bedeutet, d.h. der Text entspricht nicht der Grammatik.

      if(dynamic_cast<HiddenNonTerminal *>(&this) == NULL)
      {
        parser.trackError(&this);
      }
      
      return false;
    }

Lassen Sie uns nun die Klassen beschreiben, die von der Klasse Terminal abgeleitet sind.



Non-Terminale, versteckte und explizite

Die Hauptaufgabe der Klasse HiddenNonTerminal ist es, dynamische Instanzen der Objekte zu erzeugen und die Speicherbereinigung.

class HiddenNonTerminal: public Terminal
{
  private:
    static List<Terminal *> dynamic; // Speicherbereinigung

  public:
    HiddenNonTerminal(const string id = NULL): Terminal(id)
    {
    }

    HiddenNonTerminal(HiddenNonTerminal &ref)
    {
      eq = &ref;
    }

    virtual HiddenNonTerminal *operator~()
    {
      HiddenNonTerminal *p = new HiddenNonTerminal(this);
      dynamic.add(p);
      return p;
    }
    ...
};

Die Klasse HiddenNonTerminalOR sorgt für ein Überladen des Operators '|' (einfacher als die in der Klasse Terminal, da HiddenNonTerminalOR selbst ein "Container" ist, d.h. der Eigentümer einer Gruppe der untergeordneten Grammatikelemente).

class HiddenNonTerminalOR: public HiddenNonTerminal
{
  public:
    virtual Terminal *operator|(Terminal &t) override
    {
      Terminal *p = &t;
      next << p;
      p.setParent(&this);
      return &this;
    }
    ...
};

Die Klasse HiddenNonTerminalAND wurde in ähnlicher Weise implementiert.

Die Klasse NonTerminal stellt den überladenen Operator '=' ("Produktion" in den Regeln) bereit.

class NonTerminal: public HiddenNonTerminal
{
  public:
    NonTerminal(const string id = NULL): HiddenNonTerminal(id)
    {
    }

    virtual Terminal *operator=(Terminal &t)
    {
      Terminal *p = &t;
      while(p.getParent() != NULL)
      {
        p = p.getParent();
        if(p == &t)
        {
          Print("Cyclic dependency in assignment: ", toString(), " <<== ", t.toString());
          p = &t;
          break;
        }
      }
    
      if(dynamic_cast<HiddenNonTerminal *>(p) != NULL)
      {
        eq = p;
      }
      else
      {
        eq = &t;
      }
      eq.setParent(this);
      return &this;
    }
};

Schließlich gibt es noch die Klassen Rule — abgeleitet von NonTerminal. Ihre ganze Rolle besteht jedoch darin, einige Regeln bei der Beschreibung der Grammatik als primär (wenn sie ein Objekt Rule generieren) oder sekundär (wenn sie zu NonTerminal führen) zu markieren.

Um die Beschreibung der Non-Terminale zu erleichtern, wurden die folgenden Makros erstellt:

// debuggen
#define R(Y) (Y^__LINE__)

// freigeben
#define R(Y) (~Y)

#define _DECLARE(Cls) NonTerminal Cls(#Cls); Cls
#define DECLARE(Cls) Rule Cls(#Cls); Cls
#define _FORWARD(Cls) NonTerminal Cls(#Cls);
#define FORWARD(Cls) Rule Cls(#Cls);

Das Makroargument ist eine Zeichenfolge, ein eindeutiger Name. Eine Vorwärtsdeklaration ist erforderlich, wenn Non-Terminale sich aufeinander beziehen — das haben wir in der Grammatik des Rechners gesehen.

Darüber hinaus ist für die Generierung von Terminalen mit Token eine spezielle Klasse Keywords implementiert, die auch den Speicher bereinigt.

class Keywords
{
  private:
    static List<Terminal *> keywords;

  public:
    static Terminal *get(const TokenType t, const string value = NULL)
    {
      Terminal *p = new Terminal(t, value);
      keywords.add(p);
      return p;
    }
};

Um sie bei der Beschreibung der Grammatik zu verwenden, wurden die folgenden Makros erstellt:

#define T(X) Keywords::get(X)
#define TC(X,Y) Keywords::get(X,Y)

Sehen wir uns an, wie der Rechner die oben betrachtete Grammatik über die implementierten Programmschnittstellen beschrieben wird.

  FORWARD(expression);
  _DECLARE(value) = T(CONST_INTEGER) | T(LEFT_PAREN) + R(expression) + T(RIGHT_PAREN);
  _DECLARE(operation) = (T(PLUS) | T(STAR) | T(MINUS) | T(SLASH)) + R(expression);
  expression = R(value) + R(operation)*0;

Schlussendlich sind wir bereit, die Klasse Parser zu besprechen.


Parser

Die Klasse Parser hat Mitglieder, um die Eingabeliste der Token ('tokens'), die aktuelle Position darin ('cursor'), die am weitesten entfernte bekannte Position ('maxcursor', wird in der Fehlerdiagnose verwendet), einen Stapel von Positionen vor dem Aufruf der verschachtelten Elementgruppen ('states', zum Zurückrollen, zum Erinnern an das 'backtracking') und einen Link zum Eingabetext ('source', zum Drucken von Protokollen und für andere Zwecke) zu speichern.

class Parser
{
  private:
    BaseArray<Token *> *tokens; // Eingabestrom
    int cursor;                 // aktueller Token
    int maxcursor;
    BaseArray<int> states;
    const Source *source;

Außerdem verfolgt der Parser die gesamte Hierarchie der Aufrufe durch die Grammatikelemente mit Hilfe von 'stack'. Die in dieser Vorlage verwendete Klasse TreeNode ist ein einfacher Container für ein Paar (Terminal, Token), dessen Quellcode sich im angehängten Archiv befindet. Fehler werden für die Diagnose in einem anderen Stapel gesammelt — 'errors'.

    // aktueller Stapel, wie sich die Grammatik entwickelt.
    Stack<TreeNode *> stack;

    // beinhaltet den aktuellen Stapel mit jedem Problem
    Stack<Stack<TreeNode *> *> errors;

Der Parser-Konstruktor erhält die Liste der Token, den Quelltext und das optionale Kennzeichen, das das Bilden eines Syntaxbaums während des Parsens ermöglicht.

  public:
    Parser(BaseArray<Token *> *_tokens, const Source *text, const bool _buildTree = false)

Wenn der Baumodus aktiviert ist, werden alle erfolgreichen "Produktionen", die als Objekte von TreeNode auf den Stapel gelangt sind, auf die Baumwurzel — der Variablen 'tree' — aufgereiht:

    TreeNode *tree;   // concrete syntax tree (optional result)

Zu diesem Zweck unterstützt die Klasse TreeNode ein Array von Unterknoten. Nachdem der Parser seine Arbeit beendet hat, kann der Baum, sofern er aktiviert wurde, mit der folgenden Methode abgerufen werden:

    const TreeNode *getCST() const
    {
      return tree;
    }

Die Hauptmethode des Parsers, 'match', in ihrer vereinfachten Form, sieht wie folgt aus:

    bool match(Terminal *p)
    {
      TreeNode *node = new TreeNode(p, getToken());
      stack.push(node);
      int previous = cursor;
      bool result = p.parse(&this);
      stack.pop();
      
      if(result) // Erfolgreich
      {
        if(stack.size() > 0) // es gibt einen Halter, an den man sich binden kann.
        {
          if(cursor > previous) // Token wurde verspeist ;)
          {
            stack.top().bind(node);
          }
          else
          {
            delete node;
          }
        }
      }
      else
      {
        delete node;
      }

      return result;
    }

Die Methoden 'advance' und 'commitState', die wir gesehen haben, als wir uns mit den Klassen der Terminale vertraut gemacht haben, werden genau so implementiert (einige Details werden übersprungen).

    void advance(const Terminal *p)
    {
      production(p, cursor, tokens[cursor], stack.size());
      if(cursor < tokens.size() - 1) cursor++;
      
      if(cursor > maxcursor)
      {
        maxcursor = cursor;
        errors.clear();
      }
    }

    void commitState(const Terminal *p)
    {
      int x = states.pop();
      for(int i = x; i < cursor; i++)
      {
        production(p, i, tokens[i], stack.size());
      }
    }

'advance' bewegt den Cursor durch die Liste der Token. Wenn die Position das Maximum überschreitet, können wir die aufgelaufenen Fehler beseitigen, die bei jeder erfolglosen Prüfung aufgezeichnet werden.

Das Verfahren 'production' verwendet eine Callback-Schnittstelle, um den Benutzer des Parsers über die 'production' zu informieren — wir werden es in Tests weiter verwenden.

    void production(const Terminal *p, const int index, const Token *t, const int level)
    {
      if(callback) callback.produce(&this, p, index, t, source, level);
    }

Das Interface ist wie folgt definiert:

interface Callback
{
  void produce(const Parser *parser, const Terminal *, const int index, const Token *, const Source *context, const int level);
  void backtrack(const int index);
};

Objekte, die dieses Interface auf Seiten des Clienten implementieren, können mit der Methode setCallback mit dem Parser verbunden werden — dann wird es bei jeder 'production' aufgerufen. Alternativ kann dieses Objekt aufgrund der Überladung des Operators [Callback *] individuell mit jedem Terminal verbunden werden. Es ist nützlich für das Debugging, Haltepunkte an den spezifischen Grammatikpunkten zu setzen.

Lassen Sie uns den Parser in der Praxis ausprobieren.

Praxis, Teil 1: Rechner

Wir hatten bereits die Grammatik des Rechners. Lassen Sie uns dafür ein Debugging-Skript erstellen. Wir werden es auch nachträglich für die Tests mit der MQL-Grammatik ergänzen.

#property script_show_inputs

enum TEST_GRAMMAR {Expression, MQL};

input TEST_GRAMMAR TestMode = Expression;;
input string SourceFile = "Sources/calc.txt";;
input string IncludesFolder = "Sources/Include/";;
input bool LoadIncludes = false;
input bool PrintCST = false;

#include <mql5/scanner.mqh>
#include <mql5/prsr.mqh>

void OnStart()
{
  Preprocessor loader(SourceFile, IncludesFolder, LoadIncludes);
  if(!loader.run())
  {
    Print("Loader failed");
    return;
  }

  Scanner scanner(loader.text());
  List<Token *> *tokens = scanner.scanTokens();
  
  if(!scanner.isSuccess())
  {
    Print("Tokenizer failed");
    delete tokens;
    return;
  }

  Parser parser(tokens, loader.text(), PrintCST);

  if(TestMode == Expression)
  {
    testExpressionGrammar(&parser);
  }
  else
  {
    //...
  }
  
  delete tokens;
}

void testExpressionGrammar(Parser *p)
{
  _FORWARD(expression);
  _DECLARE(value) = T(CONST_INTEGER) | T(LEFT_PAREN) + R(expression) + T(RIGHT_PAREN);
  _DECLARE(operation) = (T(PLUS) | T(STAR) | T(MINUS) | T(SLASH)) + R(expression);
  expression = R(value) + R(operation)*0;

  while(p.match(&expression) && !p.isAtEnd())
  {
    Print("", "Unexpected end");
    break;
  }

  if(p.isAtEnd())
  {
    Print("Success");
  }
  else
  {
    p.printState();
  }

  if(PrintCST)
  {
    Print("Concrete Syntax Tree:");
    TreePrinter printer(p);
    printer.printTree();
  }

  Comment("");
}

Ziel des Skripts ist es, die übergebene Datei im Präprozessor zu lesen, sie mit dem Scanner in einen Strom von Token zu transformieren und mit dem Parser nach der angegebenen Grammatik zu suchen. Die Überprüfung erfolgt durch den Aufruf der Methode 'match', in der die Grundregel der Grammatik an 'expression', übergeben wird.

Als Option (PrintCST) können Sie den Syntaxbaum des verarbeiteten Ausdrucks mithilfe des Hilfsprogramms TreePrinter in das Log schreiben.

Achtung! Für echte Programme wird der Baum sehr groß sein. Diese Option wird nur empfohlen, wenn Sie die kleinen Fragmente der Grammatik debuggen oder wenn die gesamte Grammatik nicht groß ist, wie im Falle unseres Rechners.

Wenn wir ein Testskript für die Datei mit dem Ausdruck "(10+1)*2" ausführen, erhalten wir den folgenden Baum (denken Sie daran, TestMode=Expression und PrintCST=true auszuwählen):

Konkreter Syntaxbaum:
|  |  |Terminal LEFT_PAREN @ (
|  |   |  | |Terminal CONST_INTEGER @ 10
|  |   |  |NonTerminal value
|  |   |  |  |Terminal PLUS @ +
|  |   |  |  |  | |Terminal CONST_INTEGER @ 1
|  |   |  |  |  |NonTerminal value
|  |   |  |  |NonTerminal expression
|  |   |  |NonTerminal operation
|  |   |NonTerminal expression
|  |  |Terminal RIGHT_PAREN @ )
|  |NonTerminal value
|  |  |Terminal STAR @ *
|  |  |  | |Terminal CONST_INTEGER @ 2
|  |  |  |NonTerminal value
|  |  |NonTerminal expression
|  |NonTerminal operation
|NonTerminal expression

Vertikale Linien bezeichnen die Verarbeitungsebenen der Non-Terminale, die in der Grammatik explizit beschrieben sind, d.h. die genannten. Leerzeichen entsprechen den Ebenen, auf denen die implizit angelegten Non-Terminale der Klassen HiddenXYZ "erweitert" wurden — alle diese Knoten werden standardmäßig nicht im Protokoll angezeigt; in der Klasse TreePrinter gibt es jedoch eine Option, sie zu aktivieren.

Beachten Sie, dass die Option PrintCST auf einer speziellen Struktur mit Metadaten basiert - einem Baum von TreeNode-Objekten. Unser Parser kann es optional bei der Analyse als Antwort auf den Aufruf der Methode getCST erzeugen. Erinnern Sie sich daran, dass die Einbeziehung des Baummodus durch den dritten Parameter des Parser-Konstruktors festgelegt wird.

Sie können mit anderen Ausdrücken experimentieren, auch mit solchen, die falsch sind, um sicherzustellen, dass eine Fehlerbehandlung vorliegt. Wenn wir beispielsweise einen Ausdruck mit '10+' verunstalten, erhalten wir die folgende Meldung:

Failed
First 2 tokens read out of 3
Source: EOF (file:Sources/Include/Layouts/calc.txt; line:1; offset:4) ``
Expected:
CONST_INTEGER in expression;operation;expression;value;
LEFT_PAREN in expression;operation;expression;value;

Nun, alle Klassen funktionieren. Nun können wir zum praktischen Hauptteil — MQL-Parsing - übergehen.


Praxis, Teil 2: MQL Grammatik

Auf der technischen Seite ist alles bereit, um die MQL-Grammatik zu schreiben. Es ist jedoch viel komplizierter als ein kleiner Taschenrechner. Alles von Grund auf neu zu erstellen, wäre eine Herkulesaufgabe. Um das Problem zu lösen, lassen Sie uns die Tatsache nutzen, dass MQL ähnlich C++ ist.

Für C++ sind viele vorgefertigte Beschreibungen der Grammatik öffentlich zugänglich. Einer von ihnen ist als Datei cppgrmr.htm angehängt. Es wäre auch schwierig, es vollständig in unsere Grammatik zu übertragen. Erstens werden viele Strukturen in MQL nicht unterstützt. Zweitens gibt es oft die linke Rekursion in der Notation, weshalb die Regeln geändert werden müssen. Schließlich, drittens, ist es wünschenswert, die Größe der Grammatik zu begrenzen, da sie einen negativen Einfluss auf die Verarbeitungsrate haben kann: Es wäre vernünftig, einige selten verwendete Features optional von denen hinzufügen zu lassen, die sie wirklich brauchen.

Die Reihenfolge der Erwähnung der Alternativen von OR ist wichtig, da die erste getriggerte Version die nachfolgenden Prüfungen abfängt. Wenn Versionen unter bestimmten Bedingungen teilweise übereinstimmen, weil einige optionale Elemente übersprungen wurden, dann müssen wir sie entweder neu anordnen oder zuerst längere und spezifischere Strukturen angeben und dann die kürzeren und allgemeineren.

Lassen Sie uns zeigen, wie einige Notationen aus der htm-Datei in die Grammatik unserer Regeln und Terminale umgewandelt werden.

In C++ Grammatik:

assignment-expression:
  conditional-expression 
  unary-expression assignment-operator assignment-expression

assignment-operator: one of
  = *= /= %= += –= >= <= &= ^= |=

In MQL Grammatik:

_FORWARD(assignment_expression);
_FORWARD(unary_expression);

...

assignment_expression =
    R(unary_expression) + R(assignment_operator) + R(assignment_expression)
  | R(conditional_expression);

_DECLARE(assignment_operator) =
    T(EQUAL) | T(STAR_EQUAL) | T(SLASH_EQUAL) | T(DIV_EQUAL)
  | T(PLUS_EQUAL) | T(MINUS_EQUAL) | T(GREATER_EQUAL) | T(LESS_EQUAL)
  | T(BIT_AND_EQUAL) | T(BIT_XOR_EQUAL) | T(BIT_OR_EQUAL);

In C++ Grammatik:

unary-expression:
  postfix-expression 
  ++ unary-expression 
  –– unary-expression 
  unary-operator cast-expression 
  sizeof unary-expression 
  sizeof ( type-name ) 
  allocation-expression 
  deallocation-expression

In MQL Grammatik:

unary_expression =
    R(postfix_expression)
  | T(INC) + R(unary_expression) | T(DEC) + R(unary_expression)
  | R(unary_operator) + R(cast_expression)
  | T(SIZEOF) + T(LEFT_PAREN) + R(type) + T(RIGHT_PAREN)
  | R(allocation_expression) | R(deallocation_expression);

In C++ Grammatik:

statement:
  labeled-statement 
  expression-statement 
  compound-statement 
  selection-statement 
  iteration-statement 
  jump-statement 
  declaration-statement
  asm-statement
  try-except-statement
  try-finally-statement

In MQL Grammatik:

statement =
    R(expression_statement) | R(codeblock) | R(selection_statement)
  | R(labeled_statement) | R(iteration_statement) | R(jump_statement);

Es gibt auch eine Regel für den Deklarationsbefehl in der MQL-Grammatik. Aber es wird übertragen. Regeln wurden einfacher übernommen als in C++. Im Prinzip ist diese Grammatik ein lebendiger Organismus, oder, in englischer Sprache, "work in progress". Bei der Interpretation bestimmter Strukturen im Quellcode treten mit hoher Wahrscheinlichkeit Fehler auf.

Für die MQL-Grammatik ist der Einstiegspunkt die Regel 'program', die aus 1 oder mehreren 'elements' besteht:

  _DECLARE(element) =
     R(class_decl)
   | R(declaration_statement) | R(function) | R(sharp) | R(macro);

  _DECLARE(program) = R(element)*1;

In unserem Testskript wird die vorgestellte MQL-Grammatik in der Funktion testMQLgrammar beschrieben:

void testMQLgrammar(Parser *p)
{
  // alle Grammatikregeln kommen als erstes
  // ...
  _DECLARE(program) = R(element)*1;

Und dort wird das Parsen gestartet (ähnlich wie beim Taschenrechner):

  while(p.match(&program) && !p.isAtEnd())
  ...

Im Fehlerfall sollte das problematische Element durch Protokolle lokalisiert werden, und die spezifische Grammatikregel muss an einem separaten Eingabefragment des Textes debuggt werden (es wird empfohlen, ein Fragment mit maximal 5-6 Token zu verwenden). Mit anderen Worten, die Methode 'match' des Parsers sollte für eine bestimmte Regel aufgerufen werden, und eine Datei mit einer separaten Sprachstruktur sollte der Eingabe zugeführt werden. Um die Spuren des Parsers im Protokoll auszugeben, ist es notwendig, die Direktive im Skript zu deaktivieren:

//#define PRINTX Print

Achtung! Die Menge der anzuzeigenden Informationen ist sehr groß.

Vor dem Debuggen wird empfohlen, verschiedene Elemente der Regel in verschiedenen Zeilen zu platzieren, da dies die anonymen Instanzen der Objekte mit der eindeutigen Anzahl der Quellzeilen kennzeichnet.
Der Parser wurde jedoch nicht erstellt, um den Text auf seine Übereinstimmung mit der MQL-Syntax zu überprüfen, sondern um semantische Daten zu extrahieren. Lassen Sie uns versuchen, das zu tun.

Praxis, Teil 3: Auflistung der Methoden von Klassen und Hierarchie der Klassen

Als erste Anwendungsaufgabe, die auf MQL-Parsing basiert, wollen wir alle Methoden von Klassen auflisten. Zu diesem Zweck definieren wir eine Klasse, die das Interface Callback implementiert und die relevanten "Produktionen" aufzeichnen.

Im Prinzip wäre es logischer, auf der Grundlage eines Syntaxbaums zu analysieren. Das würde jedoch den Speicher für die Speicherung des Baums und einen separaten Algorithmus überladen, um über diesen Baum zu iterieren. Tatsächlich iteriert der Parser selbst jedoch bereits in der gleichen Reihenfolge beim Parsen des Textes darüber (da es diese Sequenz ist, in der der Baum aufgebaut würde, wenn der entsprechende Modus aktiviert wäre). Daher ist es einfacher, während der Ausführung zu parsen.

Die MQL-Grammatik hat die folgende Regel:

  _DECLARE(method) = R(template_decl)*0 + R(method_specifiers)*0 + R(type) + R(name_with_arg_list) + R(modifiers)*0;

Es besteht aus vielen anderen Non-Terminalen, die wiederum durch andere Non-Terminale offenbart werden, so dass der Syntaxbaum der Methode hoch verzweigt ist. Im Produktionsprozessor werden wir alle Fragmente, die sich auf die Non-Terminal-"Methode" beziehen, abfangen und in eine gemeinsame Zeichenkette einfügen. In dem Moment, in dem sich die nächste Produktion für ein anderes Non-Terminal herausstellt, bedeutet dies, dass die Methodenbeschreibung beendet ist und das Ergebnis im Protokoll angezeigt werden kann.

class MyCallback: public Callback
{
    virtual void produce(const Parser *parser, const Terminal *p, const int index, const Token *t, const Source *context, const int level) override
    {
      static string method = "";
      
      // sammeln aller Token der `Methode` nonterminal
      if(p.getName() == "method")
      {
        method += t.content(context) + " ";
      }
      // sobald ein anderen [Non-] Terminal erkannt wurde und die Zeichenkette befüllt wurde, Signatur ist fertig
      else if(method != "")
      {
        Print(method);
        method = "";
      }
    }

Um unseren Prozessor mit dem Parser zu verbinden, lassen Sie uns das Testskript wie folgt erweitern (in OnStart):

  MyCallback myc;
  Parser parser(tokens, loader.text(), PrintCST);
  parser.setCallback(&myc);

Zusätzlich zu der Liste der Methoden, lassen Sie uns Informationen über die Deklaration von Klassen sammeln — es wird insbesondere erforderlich sein, den Kontext zu identifizieren, in dem die Methoden definiert sind, aber wir können auch die Ableitungshierarchie aufbauen.

Um Metadaten zu einer zufälligen Klasse zu speichern, lassen Sie uns eine Klasse namens vorbereiten 'Class'. ;-)

  class Class
  {
    private:
      BaseArray<Class *> subclasses;
      Class *superclass;
      string name;

    public:
      Class(const string n): name(n), superclass(NULL)
      {
      }
      
      ~Class()
      {
        subclasses.clear();
      }
      
      void addSubclass(Class *derived)
      {
        derived.superclass = &this;
        subclasses.add(derived);
      }
      
      bool hasParent() const
      {
        return superclass != NULL;
      }
      
      Class *operator[](int i) const
      {
        return subclasses[i];
      }
      
      int size() const
      {
        return subclasses.size();
      }
      ...
   };

Sie hat ein Array mit Unterklassen und einen Link zu einer Oberklasse. Die Methode addSubclass ist für das Befüllen dieser zusammenhängenden Felder verantwortlich. Wir werden die Instanzen der Klassenobjekte in eine Zuordnung mit dem Zeichenkettenschlüssel als Klassennamen einfügen:

  Map<string,Class *> map;

Die Zuordnung befindet sich im gleichen Objekt von MyCallback.

Jetzt können wir die Methode 'produce' aus dem Interface Callback erweitern. Um Token im Zusammenhang mit der Deklaration von Klassen zu sammeln, müssen wir etwas mehr Regeln abfangen, denn wir brauchen nicht nur die vollständige Deklaration, sondern auch die mit den spezifischen Eigenschaften: Name der neuen Klasse, Typen ihrer Vorlage, falls vorhanden, der Name der Basisklasse und die Typen ihrer Vorlage, falls vorhanden.

Fügen wir die relevanten Variablen zur Datenerhebung hinzu (Achtung! Klassen in MQL können verschachtelte Klassen sein, wenn auch nicht häufig, aber wir betrachten das nicht aus Gründen der Einfachheit! Gleichzeitig unterstützt unsere MQL-Grammatik das).

      static string templName = "";
      static string templBaseName = "";
      static string className = "";
      static string baseName = "";

Im Zusammenhang mit dem Non-Terminal 'template_decl' sind Identifikatoren Vorlagentypen:

      if(p.getName() == "template_decl" && t.getType() == IDENTIFIER)
      {
        if(templName != "") templName += ",";
        templName += t.content(context);
      }

Die relevanten Grammatikregeln für 'template_decl' sowie für die unten verwendeten Objekte können in den beigefügten Quellcodes studiert werden.

Im Kontext der Non-Terminalen 'class_name' ist der Bezeichner der Klassenname; wenn templName zu diesem Zeitpunkt noch kein leerer String war, dann sind dies Vorlagentypen, die in die Beschreibung aufgenommen werden sollten:

      if(p.getName() == "class_name" && t.getType() == IDENTIFIER)
      {
        className = t.content(context);
        if(templName != "")
        {
          className += "<" + templName + ">";
          templName = "";
        }
      }

Der erste Identifikator im Kontext von 'derived_clause', falls vorhanden, ist der Name der Basisklasse.

      if(p.getName() == "derived_clause" && t.getType() == IDENTIFIER)
      {
        if(baseName == "") baseName = t.content(context);
        else
        {
          if(templBaseName != "") templBaseName += ",";
          templBaseName += t.content(context);
        }
      }

Alle nachfolgenden Identifikatoren sind die Vorlagentypen der Basisklasse.

Sobald die Klassendeklaration abgeschlossen ist, wird die Regel 'class_decl' in der Grammatik ausgelöst. Zu diesem Zeitpunkt sind alle Daten bereits gesammelt und können der Klassenkarte hinzugefügt werden.

      if(p.getName() == "class_decl") // Finalisieren
      {
        if(className != "")
        {
          if(map[className] == NULL)
          {
            map.put(className, new Class(className));
          }
          else
          {
            // Klasse bereits definiert, vielleicht durch eine Vorwärtsdeklaration
          }
        }
      
        if(baseName != "")
        {
          if(templBaseName != "")
          {
            baseName += "<" + templBaseName + ">";
          }
          Class *base = map[baseName];
          if(base == NULL)
          {
            // Unbekannte Klasse, vielleicht nicht eingebunden, hmm, komisch
            base = new Class(baseName);
            map.put(baseName, base);
          }
          
          if(map[className] == NULL)
          {
            Print("Error: base name `", baseName, "` resolved before declaration of the class: ", className);
          }
          else
          {
            base.addSubclass(map[className]);
          }
          
          baseName = "";
        }
        className = "";
        templName = "";
        templBaseName = "";
      }

Am Ende löschen wir alle Zeichenketten und warten, bis die nächsten Deklarationen erscheinen.

Nach dem erfolgreichen Parsen eines Programmtextes bleibt es dabei, die Hierarchie der Klassen komfortabel darzustellen. Im Testskript bietet die Klasse MyCallback die Funktion 'print' zur Anzeige im Protokoll. Sie wiederum verwendet die Methode 'print' in den Objekten der Klasse 'Class'. Wir werden diese Hilfsalgorithmen für das eigenständige Lesen und darüber hinaus als ein weiteres kleines Programmierproblem für diejenigen, die ihre Stärken ausspielen wollen (solche Wettbewerbe treten oft spontan im Forum von mql5.com auf). Die bestehende Implementierung ist rein pragmatisch und erhebt nicht den Anspruch, optimal zu sein. Es sichert einfach das Ziel: Die baumartige Hierarchie der klassenartigen Objekte im Protokoll so stark wie möglich darzustellen. Dies kann jedoch effizienter erfolgen.

Lassen Sie uns überprüfen, wie das Testskript funktioniert, um einige MQL-Projekte zu analysieren. Im Folgenden setzen wir den Parameter TestMode = MQL.

Zum Beispiel für den standardmäßigen Expert Advisor 'MACD Sample.mq5', SourceFile = Sources/Experts/Examples/MACD/MACD Sample.mq5 und LoadIncludes = true, d.h. mit allen Abhängigkeiten, eingestellt hat, erhalten wir das folgende Ergebnis (die Liste der Methoden ist weitgehend verkürzt):

Processing Sources/Experts/Examples/MACD/MACD Sample.mq5
Scanning...
#include <Trade\Trade.mqh>
Including Sources/Include/Trade\Trade.mqh
#include <Object.mqh>
Including Sources/Include/Object.mqh
#include "StdLibErr.mqh"
Including Sources/Include/StdLibErr.mqh
#include "OrderInfo.mqh"
Including Sources/Include/Trade/OrderInfo.mqh
#include <Object.mqh>
Including Sources/Include/Object.mqh
#include "SymbolInfo.mqh"
Including Sources/Include/Trade/SymbolInfo.mqh
#include <Object.mqh>
Including Sources/Include/Object.mqh
#include "PositionInfo.mqh"
Including Sources/Include/Trade/PositionInfo.mqh
#include <Object.mqh>
Including Sources/Include/Object.mqh
#include "SymbolInfo.mqh"
Including Sources/Include/Trade/SymbolInfo.mqh
#include <Trade\PositionInfo.mqh>
Including Sources/Include/Trade\PositionInfo.mqh
#include <Object.mqh>
Including Sources/Include/Object.mqh
#include "SymbolInfo.mqh"
Including Sources/Include/Trade/SymbolInfo.mqh
Files processed: 8
Source length: 175860
File map:
Sources/Experts/Examples/MACD/MACD Sample.mq5 0
Sources/Include/Trade\Trade.mqh 900
Sources/Include/Object.mqh 1277
Sources/Include/StdLibErr.mqh 1657
Sources/Include/Object.mqh 2330
Sources/Include/Trade\Trade.mqh 3953
Sources/Include/Trade/OrderInfo.mqh 4004
Sources/Include/Trade/SymbolInfo.mqh 4407
Sources/Include/Trade/OrderInfo.mqh 38837
Sources/Include/Trade\Trade.mqh 59925
Sources/Include/Trade/PositionInfo.mqh 59985
Sources/Include/Trade\Trade.mqh 75648
Sources/Experts/Examples/MACD/MACD Sample.mq5 143025
Sources/Include/Trade\PositionInfo.mqh 143091
Sources/Experts/Examples/MACD/MACD Sample.mq5 158754
Lines: 4170
Tokens: 18005
Defining grammar...
Parsing...
CObject :: CObject * Prev ( void ) const 
CObject :: void Prev ( CObject * node ) 
CObject :: CObject * Next ( void ) const 
CObject :: void Next ( CObject * node ) 
CObject :: virtual bool Save ( const int file_handle ) 
CObject :: virtual bool Load ( const int file_handle ) 
CObject :: virtual int Type ( void ) const 
CObject :: virtual int Compare ( const CObject * node , const int mode = 0 ) const 
CSymbolInfo :: string Name ( void ) const 
CSymbolInfo :: bool Name ( const string name ) 
CSymbolInfo :: bool Refresh ( void ) 
CSymbolInfo :: bool RefreshRates ( void ) 

...

CSampleExpert :: bool Init ( void ) 
CSampleExpert :: void Deinit ( void ) 
CSampleExpert :: bool Processing ( void ) 
CSampleExpert :: bool InitCheckParameters ( const int digits_adjust ) 
CSampleExpert :: bool InitIndicators ( void ) 
CSampleExpert :: bool LongClosed ( void ) 
CSampleExpert :: bool ShortClosed ( void ) 
CSampleExpert :: bool LongModified ( void ) 
CSampleExpert :: bool ShortModified ( void ) 
CSampleExpert :: bool LongOpened ( void ) 
CSampleExpert :: bool ShortOpened ( void ) 
Success
Class hierarchy:

CObject
  ^
  +--CSymbolInfo
  +--COrderInfo
  +--CPositionInfo
  +--CTrade
  +--CPositionInfo

CSampleExpert

Versuchen wir uns an einem Projekt Dritter. Ich entschied mich für den EA 'SlidingPuzzle2' aus diesem Artikel. Ich kopierte ihn nach hier: "SourceFile = Sources/Experts/Examples/Layouts/SlidingPuzzle2.mq5". Nachdem alle Header-Dateien (LoadIncludes = true) eingebunden wurden, erhalten wir folgendes Ergebnis (verkürzt):

Processing Sources/Experts/Examples/Layouts/SlidingPuzzle2.mq5
Scanning...
#include "SlidingPuzzle2.mqh"
Including Sources/Experts/Examples/Layouts/SlidingPuzzle2.mqh
#include <Layouts\GridTk.mqh>
Including Sources/Include/Layouts\GridTk.mqh
#include "Grid.mqh"
Including Sources/Include/Layouts/Grid.mqh
#include "Box.mqh"
Including Sources/Include/Layouts/Box.mqh
#include <Controls\WndClient.mqh>
Including Sources/Include/Controls\WndClient.mqh
#include "WndContainer.mqh"
Including Sources/Include/Controls/WndContainer.mqh
#include "Wnd.mqh"
Including Sources/Include/Controls/Wnd.mqh
#include "Rect.mqh"
Including Sources/Include/Controls/Rect.mqh
#include <Object.mqh>
Including Sources/Include/Object.mqh
#include "StdLibErr.mqh"
Including Sources/Include/StdLibErr.mqh
#include "Scrolls.mqh"
Including Sources/Include/Controls/Scrolls.mqh
#include "WndContainer.mqh"
Including Sources/Include/Controls/WndContainer.mqh
#include "Panel.mqh"
Including Sources/Include/Controls/Panel.mqh
#include "WndObj.mqh"
Including Sources/Include/Controls/WndObj.mqh
#include "Wnd.mqh"
Including Sources/Include/Controls/Wnd.mqh
#include <Controls\Edit.mqh>
Including Sources/Include/Controls\Edit.mqh
#include "WndObj.mqh"
Including Sources/Include/Controls/WndObj.mqh
#include <ChartObjects\ChartObjectsTxtControls.mqh>
Including Sources/Include/ChartObjects\ChartObjectsTxtControls.mqh
#include "ChartObject.mqh"
Including Sources/Include/ChartObjects/ChartObject.mqh
#include <Object.mqh>
Including Sources/Include/Object.mqh
Files processed: 17
Source length: 243134
File map:
Sources/Experts/Examples/Layouts/SlidingPuzzle2.mq5 0
Sources/Experts/Examples/Layouts/SlidingPuzzle2.mqh 493
Sources/Include/Layouts\GridTk.mqh 957
Sources/Include/Layouts/Grid.mqh 1430
Sources/Include/Layouts/Box.mqh 1900
Sources/Include/Controls\WndClient.mqh 2377
Sources/Include/Controls/WndContainer.mqh 2760
Sources/Include/Controls/Wnd.mqh 3134
Sources/Include/Controls/Rect.mqh 3509
Sources/Include/Controls/Wnd.mqh 14312
Sources/Include/Object.mqh 14357
Sources/Include/StdLibErr.mqh 14737
Sources/Include/Object.mqh 15410
Sources/Include/Controls/Wnd.mqh 17033
Sources/Include/Controls/WndContainer.mqh 46214
Sources/Include/Controls\WndClient.mqh 61689
Sources/Include/Controls/Scrolls.mqh 61733
Sources/Include/Controls/Panel.mqh 62137
Sources/Include/Controls/WndObj.mqh 62514
Sources/Include/Controls/Panel.mqh 72881
Sources/Include/Controls/Scrolls.mqh 78251
Sources/Include/Controls\WndClient.mqh 103907
Sources/Include/Layouts/Box.mqh 115349
Sources/Include/Layouts/Grid.mqh 126741
Sources/Include/Layouts\GridTk.mqh 131057
Sources/Experts/Examples/Layouts/SlidingPuzzle2.mqh 136066
Sources/Include/Controls\Edit.mqh 136126
Sources/Include/ChartObjects\ChartObjectsTxtControls.mqh 136555
Sources/Include/ChartObjects/ChartObject.mqh 137079
Sources/Include/ChartObjects\ChartObjectsTxtControls.mqh 177423
Sources/Include/Controls\Edit.mqh 213551
Sources/Experts/Examples/Layouts/SlidingPuzzle2.mqh 221772
Sources/Experts/Examples/Layouts/SlidingPuzzle2.mq5 241539
Lines: 6102
Tokens: 27248
Defining grammar...
Parsing...
CRect :: CPoint LeftTop ( void ) const 
CRect :: void LeftTop ( const int x , const int y ) 
CRect :: void LeftTop ( const CPoint & point ) 

...

CSlidingPuzzleDialog :: virtual bool Create ( const long chart , const string name , const int subwin , const int x1 , const int y1 , const int x2 , const int y2 ) 
CSlidingPuzzleDialog :: virtual bool OnEvent ( const int id , const long & lparam , const double & dparam , const string & sparam ) 
CSlidingPuzzleDialog :: void Difficulty ( int d ) 
CSlidingPuzzleDialog :: virtual bool CreateMain ( const long chart , const string name , const int subwin ) 
CSlidingPuzzleDialog :: virtual bool CreateButton ( const int button_id , const long chart , const string name , const int subwin ) 
CSlidingPuzzleDialog :: virtual bool CreateButtonNew ( const long chart , const string name , const int subwin ) 
CSlidingPuzzleDialog :: virtual bool CreateLabel ( const long chart , const string name , const int subwin ) 
CSlidingPuzzleDialog :: virtual bool IsMovable ( CButton * button ) 
CSlidingPuzzleDialog :: virtual bool HasNorth ( CButton * button , int id , bool shuffle = false ) 
CSlidingPuzzleDialog :: virtual bool HasSouth ( CButton * button , int id , bool shuffle = false ) 
CSlidingPuzzleDialog :: virtual bool HasEast ( CButton * button , int id , bool shuffle = false ) 
CSlidingPuzzleDialog :: virtual bool HasWest ( CButton * button , int id , bool shuffle = false ) 
CSlidingPuzzleDialog :: virtual void Swap ( CButton * source ) 
Success
Class hierarchy:

CPoint

CSize

CRect

CObject
  ^
  +--CWnd
  |    ^
  |    +--CDragWnd
  |    +--CWndContainer
  |    |    ^
  |    |    +--CScroll
  |    |    |    ^
  |    |    |    +--CScrollV
  |    |    |    +--CScrollH
  |    |    +--CWndClient
  |    |         ^
  |    |         +--CBox
  |    |              ^
  |    |              +--CGrid
  |    |                   ^
  |    |                   +--CGridTk
  |    +--CWndObj
  |         ^
  |         +--CPanel
  |         +--CEdit
  +--CGridConstraints
  +--CChartObject
       ^
       +--CChartObjectText
            ^
            +--CChartObjectLabel
                 ^
                 +--CChartObjectEdit
                 |    ^
                 |    +--CChartObjectButton
                 +--CChartObjectRectLabel

CAppDialog
  ^
  +--CSlidingPuzzleDialog

Hier ist die Hierarchie der Klassen interessanter.

Obwohl ich den Parser in verschiedenen Projekten getestet habe, wird er bei bestimmten Programmen mehr als wahrscheinlich "stolpern". Eines der Probleme, die darin noch nicht gelöst sind, betrifft die Verarbeitung von Makros. Wie bereits oben erwähnt, schlägt die korrekte Analyse eine dynamische Interpretation und Erweiterung von Makros vor, wobei die Ergebnisse vor Beginn des Parsen in den Quellcode ersetzt werden.

In der aktuellen MQL-Grammatik haben wir versucht, den Aufruf der Makros als weniger strengen Aufruf von Funktionen zu definieren. Es funktioniert jedoch bei weitem nicht immer.

In der Bibliothek TypeToBytes werden beispielsweise die Parameter von Makros verwendet, um Metatypen zu erzeugen. Hier ist einer der Fälle:

#define _C(A, B) CASTING<A>::Casting(B)

Im Weiteren wird dieses Makro wie folgt verwendet:

Res = _C(STRUCT_TYPE<T1>, Tmp);

Wenn wir versuchen, den Parser auf diesem Code auszuführen, kann er STRUCT_TYPE<T1> nicht "verdauen", da dieser Parameter in der Realität einen Vorlagen-Typ darstellt, während der Parser einen Wert oder, allgemeiner gesagt, einen Ausdruck impliziert (und insbesondere die Zeichen '<' und '>' als Vergleichsoperatoren interpretiert). Nun wird ein ähnliches Problem durch den Aufruf von Makros erzeugt, danach gibt es kein Semikolon mehr. Wir können es jedoch umgehen, wenn wir ';' in den zu verarbeitenden Quellcode eingefügt haben.

Diejenigen, die es wollen, können das Experiment Nummer 3 durchführen (die ersten beiden wurden am Anfang dieses Artikels erwähnt), das darin bestehen würde, nach einem Iterationsalgorithmus zu suchen, um die aktuelle Grammatik mit solchen Makrosregeln zu modifizieren, die es Ihnen ermöglichen würden, solche komplizierten Fälle erfolgreich zu analysieren.

Schlussfolgerungen

Wir haben die allgemein und technologisch einfachste Art des Datenparsing betrachtet, einschließlich der Analyse der in MQL geschriebenen Quellcodes. Zu diesem Zweck wurde die Grammatik der MQL-Sprache vorgestellt, sowie die Implementierung von Standardwerkzeugen wie Parser und Scanner. Die Verwendung dieser Elemente zur Erlangung der Quellcode-Struktur ermöglicht die Berechnung ihrer Statistiken, die Identifizierung der Qualitätsindikatoren, die Darstellung von Abhängigkeiten und die automatische Änderung der Formate.

Gleichzeitig erfordert die hier vorgestellte Implementierung einige Verbesserungen, um eine 100%ige Kompatibilität mit komplexen MQL-Projekten zu erreichen, insbesondere im Hinblick auf die Unterstützung der Erweiterung der Makros.

Im Falle einer tieferen Vorbereitung, wie z.B. dem Speichern von Informationen über die in der Namenstabelle enthaltenen Entitäten, würde dieser Ansatz auch die Durchführung der Codegenerierung, die Kontrolle typischer Fehler und die Durchführung anderer, komplizierterer Aufgaben ermöglichen.


Übersetzt aus dem Russischen von MetaQuotes Software Corp.
Originalartikel: https://www.mql5.com/ru/articles/5638

Beigefügte Dateien |
MQL5PRSR.zip (33.46 KB)
Die Stärke von ZigZag (Teil II). Beispiele für das Empfangen, Verarbeiten und Anzeigen von Daten Die Stärke von ZigZag (Teil II). Beispiele für das Empfangen, Verarbeiten und Anzeigen von Daten

Im ersten Teil der Artikelserie habe ich einen modifizierten ZigZag-Indikator und eine Klasse zum Empfangen von Daten dieser Art von Indikatoren beschrieben. Hier werde ich zeigen, wie man Indikatoren entwickelt, die auf diesen Tools basieren, und ein EA für Tests schreiben, der gemäß den Signalen des ZigZag-Indikators handelt. Als Ergänzung wird der Artikel eine neue Version der Bibliothek EasyAndFast zur Entwicklung grafischer Benutzeroberflächen vorstellen.

Die Entwicklung von grafischen Oberflächen für Expert Advisors und Indikatoren auf Basis von .Net Framework und C# Die Entwicklung von grafischen Oberflächen für Expert Advisors und Indikatoren auf Basis von .Net Framework und C#

Der Artikel stellt eine einfache und schnelle Methode zur Erstellung von grafischen Fenstern mit Visual Studio mit anschließender Integration in den MQL-Code des Expert Advisors vor. Der Artikel richtet sich an ein nicht spezialisiertes Publikum und erfordert keine Kenntnisse der Technologie von C# oder .Net.

Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil I). Konzept, Datenverwaltung und erste Ergebnisse Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil I). Konzept, Datenverwaltung und erste Ergebnisse

Bei der Analyse einer Vielzahl von Handelsstrategien, der Entwicklung von Anwendungen für MetaTrader 5 und MetaTrader 4 Terminals und verschiedenen MetaTrader Websites kam ich zu dem Schluss, dass diese ganze Vielfalt hauptsächlich auf den gleichen elementaren Funktionen, Aktionen und Werten basiert, die regelmäßig in verschiedenen Programmen wiederkehren. Daraus entstand die plattformübergreifende Bibliothek DoEasy für die einfache und schnelle Entwicklung von Anwendungen für МetaТrader 4 und 5.

Untersuchung von Techniken zur Analyse der Kerzen (Teil II): Automatische Suche nach den Mustern Untersuchung von Techniken zur Analyse der Kerzen (Teil II): Automatische Suche nach den Mustern

Im vorherigen Artikel haben wir 14 Muster analysiert, die aus einer Vielzahl von bestehenden Kerzenformationen ausgewählt wurden. Es ist unmöglich, alle Muster einzeln zu analysieren, deshalb wurde eine andere Lösung gefunden. Das neue System sucht und testet neue Kerzenmuster basierend auf bekannten den Kerzentypen.