[Split von 'Eigene Klassenbibliothek'] Diskussionen um Parser



  • volkard schrieb:

    edit: Beispiele:
    Rekursive Abstiegscompiler: Offensichtlich das, was man im Sinne hat, aber ein Funktionsaufruf/-rücksprung kostet "Sprungziel lesen, IP schreiben, SP inkrementieren, IP=Sprungziel, SP für lokale Variablen erhöhen"/"SP wieder erniedrigen, Rücksprungziel lesen, SP dekrementieren, IP=Rücksprungziel" oder so, unendlich teuer, man baut 50 Jahre lang endliche Automaten und so, um mehr Speed zu haben. Nu haben die Prozessorbauer in die Befehle call/ret so hammerviele Transistoren reingesteckt, der native Stil ist auf einmal besser.

    Kannst du das etwas ausführlicher erklären? Das soll nur mit der Performance zu tun haben? Was ist mit der unterschiedlichen Mächtigkeit der Methoden?



  • Bashar schrieb:

    volkard schrieb:

    edit: Beispiele:
    Rekursive Abstiegscompiler: Offensichtlich das, was man im Sinne hat, aber ein Funktionsaufruf/-rücksprung kostet "Sprungziel lesen, IP schreiben, SP inkrementieren, IP=Sprungziel, SP für lokale Variablen erhöhen"/"SP wieder erniedrigen, Rücksprungziel lesen, SP dekrementieren, IP=Rücksprungziel" oder so, unendlich teuer, man baut 50 Jahre lang endliche Automaten und so, um mehr Speed zu haben. Nu haben die Prozessorbauer in die Befehle call/ret so hammerviele Transistoren reingesteckt, der native Stil ist auf einmal besser.

    Kannst du das etwas ausführlicher erklären? Das soll nur mit der Performance zu tun haben? Was ist mit der unterschiedlichen Mächtigkeit der Methoden?

    Kann ich nicht. Bin wie viele Computerfreaks seit immer ganz kurz davor, das ultimative Computerspiel oder das ultimative Betriebssystem oder den ultimativen Compiler zu schreiben, stets einigermaßen am Ball geblieben, und immer mal in jeder Richtung ein paar Monate heftig geplant und geforscht und Änsätze ausprobiert. Hab das Gefühl, daß der rekursive Abstiegscompiler genau alles kann, weil ich die Sprache ja auch rekursiv definiere und er ihr so blind folgen kann. Ich weiß nicht, es fühlt sich für mich so unglaublich flutschig an, der Code sprudelt in die Tastatur und bis auf gelegentliche Tippfrhler ist er fertig. Wie man Yacc/Bison benutzt, habe ich immer gelesen, denn keine Compilerbau-Vorlesung macht es anders. Habe aber auch WernerSolomons mathematische Parser gelesen und mir dazugedacht, wie man die ausbauen könnte. Ähm, Werner bringt es direkt auf den Punkt und es ist (meinem Gefühl nach) optimal schnell (ok, ich würde filemapping und char* benutzen, eine geringe Sache von O(<iostream>*n). Es ist mir nicht vorstellbar, daß der rekursive Abstiegscompiler irgendwann sagen würde "Äätsch, DAS PROBLEM kann ich aber nicht fassen und jetzt tu mal einen elend lahmen Algo benutzen, um es zu erreichen". Die Beschleunigungen, Yacc/Bison vorgenannt, aber auch so Tricks wie Ausdrücke anhand ihrer Prioritäten mal klammern(1) sind so unglaublich schwach.

    (1)

    1+2*3+2^4*5
    

    wird zu linear von links nach rechts gelesen, jeder Operator hat eine Prio.

    1+2*3+2^4*5
     1 2 1 3 2
    

    Nu geht man linear durch und tut pro aufsteigender Prio vor dem Atom eine ( setzen und pro absteigener eine ) nach dem Atom.

    [code]
    (1+(2*3)+((2^4)*5))
      1  2  1   3  2
    

    Supi, die ganze Kacke wurde analysiert, ohne einen extrem teuren Stack verwenden zu müssen (ich rede von Algol68 und passenden Rechnern). Nu kann im zweiten Durchlauf (leider mit Stack) schnell das Endergebnis berechnet werden. Erst über den ganzen Ausdruck zu laufen um ihn zu analysieren und dann erst nichmal drüber zu laufen, um ihn auszuwerten, das geht nicht mehr gut. Manchmal ist das RAM 300-mal lahmer als der Prozessor. Für die Platte wollen wir gar nicht dran denken.
    edit: Schwach? Kann sein, daß man damit auch alles könnte, aber ich zu dumm bin, bei so einem Vorgehen noch ein paar primitive Typen oder gar Templates reinzupressen. Es täte mir aber sehr weh, das zu machen. Ohne das Gefühl, damit Performance zu gewinnen. Es täte mir sehr sehr weh, Kopf->Betonwand und ich hätte stets das Gefühl, stattdessen schnell das P=NP-Problem lösen zu sollen, das wäre einfacher.



  • volkard schrieb:

    Kann ich nicht. Bin wie viele Computerfreaks seit immer ganz kurz davor, das ultimative Computerspiel oder das ultimative Betriebssystem oder den ultimativen Compiler zu schreiben, stets einigermaßen am Ball geblieben, und immer mal in jeder Richtung ein paar Monate heftig geplant und geforscht und Änsätze ausprobiert. Hab das Gefühl, daß der rekursive Abstiegscompiler genau alles kann, weil ich die Sprache ja auch rekursiv definiere und er ihr so blind folgen kann.

    Das stimmt halt nicht ganz. Um es mal kurz und unpräzise zusammenzufassen: Ein rekursiver Abstiegsparser ist eine Top-Down-Methode, d.h. er baut den Ableitungsbaum von oben nach unten auf. Du steckst also in irgendeiner Funktion, die ein Nichtterminal matchen soll. Angenommen, es gibt mehrere Produktionsregeln zur Auswahl, welche nimmst du zum weiteren Abstieg? Du hast keine Chance, du musst fast blind eine auswählen. Du hast nur das aktuelle Token (vielleicht noch ein paar mehr) zur Verfügung, diese Information muss ausreichen. Die Alternative sind Bottom-Up-Methoden, die den Baum von unten her aufbauen, also Blätter sammeln, zusammenfassen, weiter sammeln, zusammenfassen, oh ein Startsymbol! fertig. Hier muss die Entscheidung für die jeweilige Produktionsregel immer erst getroffen werden, wenn die rechte Seite einer Regel komplett vorliegt. Die Klasse der so erkennbaren Sprachen ist echt größer als die der Sprachen, die durch Top-Down-Parser erkannt werden können. Außerdem sind Bottom-Up-Parser besser bei der Fehlerbehandlung bzw. Fehlerkorrektur.

    Beide können als tabellengesteuerte Parser generiert werden, der Top-Down-Parser zusätzlich rekursiv und von Hand.

    Ich nehme folgende Gründe dafür an [Spekulation!], dass man heute öfter auf die Vorteile von Bottom-Up-Methoden verzichtet:

    Das mit der Mächtigkeit erübrigt sich, wenn man Backtracking zulässt, dh die Möglichkeit, alle in Frage kommenden Produktionsregeln durchzuprobieren und nötigenfalls zurückzugehen. Das ist zwar ineffizient, theoretisch sogar katastrophal ineffizient, aber für praktisch vorkommende Sprachen anscheinend kein Problem. Kurzes Überfliegen von Wikipedia sagt mir auch, dass das Problem anscheinend gelöst wurde, nur ob diese Lösung in der Praxis verwendet wird, weiß ich nicht.

    Die Relevanz guter Fehlermeldungen ist heute auch eine andere als in den 60ern. Damals war es gut, wenn der Compiler möglichst alle Fehler gefunden hat, damit man die in seinen Lochkarten korrigieren konnte und das nächste Mal, wenn man an den Rechner durfte, möglichst ein fehlerfreies Programm hatte. Heute korrigieren wir den ersten Fehler und versuchen es sofort nochmal.

    Was könnte der Vorteil von Top-Down-Methoden oder besser gesagt von einem handgeschriebenem Parser sein? Ich nehme an u.a. die Möglichkeit, in den Parser gewisse Hacks einzubauen, wie die Unterscheidungsregeln zwischen Funktions- und Variablendeklarationen in C++. Vielleicht kann man sich auch bei den Fehlermeldungen darauf konzentrieren, die erste möglichst ausführlich anstatt sie insgesamt möglichst vollständig zu machen.

    Ein Nachteil eines handgeschriebenen Parsers ist, zumindest wenn er durch Hacks oder aus sonstigen Gründen von der zugrundeliegenden Grammatik abweicht, dass er sehr leicht fehlerhaft und inkonsistent sein kann, ohne dass man davon was merkt.

    Zu dem Beispiel mit dem Einfügen von Klammern: Die Methode kenn ich nicht, wer macht das denn so? Das sieht mir nach einem Operator precedence parser aus, der funktioniert ungefähr so. Du kannst aber nach dem Erkennen eines Ausdrucks -- also dann, wenn du eine schließende Klammer setzen willst -- beliebige Aktionen ausführen, insbesondere das, was du sowieso vorhattest, anstatt erstmal Klammern zu setzen, den Ausdruck erneut zu durchlaufen, und dann erst was zu machen.



  • volkard schrieb:

    Xin schrieb:

    wobei einige hier schreiben, als hätten sie die Wahrheit für sich gepachtet.

    *meld*

    Bei Dir weiß ich nie genau, ob Du gerade ironisch bist oder das ernst meinst...* 😃

    volkard schrieb:

    Xin schrieb:

    Ich habe ein Buch von Jürgen Wolf. Ich finde es inspirierend.

    Es gibt nicht viele Ledernacken wie uns. Die meisten gehen davon einfach kaputt.

    Ich höre mir hier immer an, wie schlecht Jürgen Wolf schreibt. Ich habe mir das C von A bis Z mal zur Hand genommen und fand, man kann es durchaus kaufen. Steht was zu C drin und auch wie es dann weitergeht. Fand ich gut.
    Ich lese aber keine C-Bücher mehr. Ich lese keine Listings mehr. Ich suche Überschriften, Inspiration. Ich muss dafür ein Buch aber nicht mehr durcharbeiten und auf die Details achten.
    Und wenn doch: Wenn ich feststellte, dass ein Buch falsch ist, dann habe ich verstanden, was man falsch machen kann - damit habe ich mehr gelernt, als hätte ich nur gelernt, wie man es richtig machen kann.
    Vielleicht verstehe ich ein Buch eher als Wegweiser, nicht als Navigationssystem: Man muss auch noch selbst überlegen, an welcher Kreuzung man in welche Richtung abbiegen sollte, statt sich alles vorkauen zu lassen.

    volkard schrieb:

    Xin schrieb:

    Genauso kann ich mir eine alte Lib angucken und darin Dinge finden, die vom ersten Eindruck heute vielleicht altmodisch aussehen. Und morgen vielleicht State of Art sind.

    Jo, kommt vor. Hier wohl nicht.

    Ich habe den Quelltext nicht gesehen. Vielleicht ist da was Cooles unter einer fragwürdigen Architektur versteckt.

    volkard schrieb:

    es fühlt sich für mich so unglaublich flutschig an, der Code sprudelt in die Tastatur und bis auf gelegentliche Tippfrhler ist er fertig.

    *...wobei ich Deinen Humor gelegentlich durchaus schätze. ^^

    volkard schrieb:

    Wie man Yacc/Bison benutzt, habe ich immer gelesen, denn keine Compilerbau-Vorlesung macht es anders.

    Ich schreibe einen Compiler, ich hatte Compilerbau in der Vorlesung, ich glaube, etwas Yacc/Bison-mäßiges schreiben zu können, aber weiß es nicht, weil ich Yacc und Bison noch nie benutzt habe.

    volkard schrieb:

    Habe aber auch WernerSolomons mathematische Parser gelesen

    Gibt's da was wirklich suchbares? Google lieferte mir dazu nix.

    Ich lese Ausdrücke nach Priorität ein. Dafür gibt es einen Mini-Stack, so dass ich keine Funktionsaufrufe brauche:

    Die Werte haben bei mir auch Priorität und zwar die höchste, da sie keine Kinder haben können: Es sind also grundsätzlich die Blätter.

    1+2*3+2^4*5
    91929193929
    

    Ich lese 1, mein Einstieg in den AST: 1. Dann +, die Zahl hat eine höhere Prio als +, also schiebt das + sich vor die Zahl und es wird eine weiterer Ausdruck erwartet: +(1,?) und lese 2: +(1, 2). Ich halte mich an der soeben gelesenen 2 fest und lese . Es entsteht ein neuer Knoten, der das Blatt, an dem ich mich festhalte als linkes Kind erhält: +(1,(2,?)). * hat aber eine höhere Prio als +, also schiebt es sich nicht vor das +, es passiert also nix: +(1,(2,?)) und ich lese die 3: +(1,(2,3)). 3 hat eine höhere Prio als das , es bleibt also dabei, ich halte mich an der soeben gelesen 3 fest und lese +:
    +(1,
    (2,+(3,?)). + hat eine niedriger Prio als , schiebt sich also vor das , mein Linkes Kind wird das Rechte Kind von , * wird mein linkes Kind: +(1,+((2,3),?)). Das neue + hat die gleiche Prio wie das erste: Passt. Ich lese die 2: +(1,+((2,3),2)) und halte mich an ihr fest. Ich lese ^: +(1,+((2,3),^(2,?))), ^ hat eine höhere Prio als das darüber liegende + - passt - und ich lese die 4: +(1,+((2,3),^(2,4))) und halte mich an der 4 fest. Es folgt : +(1,+((2,3),^(2,(4,?)))). * hat eine niedriger Prio als ^, schiebt sich also nach oben, mein linkes Kind, wird das rechte Kind von ^, ^ wird mein linkes Kind: +(1,+((2,3),(^(2,4),?))). * hat eine höhere Prio als +, bleibt also da, ich lese die nächsten Ausdruck ein: 5. +(1,+((2,3),(^(2,4),5))).
    Ich halte mich an der 5 fest und lese das nächste Token: Ende. Mein Ausdruck ist fertig: +(1,+((2,3),(^(2,4),5)))

    Das Ganze läuft in einem Durchgang, der Operator ist egal, die Prio kann direkt aus einer Tabelle ausgelesen werden: Prio[Token::Plus], fertig. Die Klammerungs-Methode haben wir in Compiler-Construction im Studium gelernt und damit den Compiler aufgebaut. Das war mir zu umständlich und zu statisch, zumal ich die Anforderung habe, die Prioritäten zur Laufzeit modifizieren zu können. Um den Ausdruck anders aufzubauen, muss ich nur meine Prioritäten-Tabelle überarbeiten.

    Da ich inzwischen mehr Operatortypen habe, bin ich trotzdem zu einem rekursiven Einlesen zurückgekehrt. Das waren rund 10 Zeilen in einer Schleife und lief mit den einfachen Priority-Operatoren problemlos.



  • Xin schrieb:

    volkard schrieb:

    Habe aber auch WernerSolomons mathematische Parser gelesen

    Gibt's da was wirklich suchbares? Google lieferte mir dazu nix.

    Dann haste bestimmt auch "Salomon" falsch geschrieben.
    http://www.c-plusplus.net/forum/p1642138



  • Bashar schrieb:

    Das stimmt halt nicht ganz. …

    Danke für die Richtigstellung. Mächtigeres Werkzeug ist eigentlich immer gut, vielleicht kicke ich die rekursiven Ideen.



  • Wenn ich mir andere Parsertypen ansehe, gewinnt RecDesc einfach immer in Einfachheit und Lesbarkeit. Fehlerbehandlung funktioniert genauso gut wie mit anderen Parsern, wenn nicht sogar besser.

    @Xin
    Dynamische Operatoren hab ich bei meinem Parser mittels Hack implementiert. Zunaechst werden Ausdruecke mit Operatoren im Syntaxbaum als "operator sequence" repraesentiert. Diese OpSeqs sind im Wesentlichen nur eine Liste an Ausdruecken mit Operatoren dazwischen. Sobald alles geparsed wurde, sind Benutzerdefinierte Operatoren samt Prioritaeten bekannt und die Operator Sequence wird in einem zweiten Parserschritt nach Prioritaet und Assiziativitaet korrekt zerlegt. Danach gehts ganz normal weiter mit semantischer Analyse. Was haeltst du von diesem Ansatz? 🤡



  • Kellerautomat schrieb:

    Dynamische Operatoren hab ich bei meinem Parser mittels Hack implementiert.

    Da wäre erstmal die Frage, was eigentlich dynamische Operatoren sind. Ich vermute selbstdefinierte, wobei sich dann die Frage stellt, wie dynamische Operatoren in Deiner Sprache aussehen, so dass sie sich von einer Funktion merklich unterscheiden.

    Selbst definierbare Operatoren habe ich (noch) nicht.

    Kellerautomat schrieb:

    Zunaechst werden Ausdruecke mit Operatoren im Syntaxbaum als "operator sequence" repraesentiert. Diese OpSeqs sind im Wesentlichen nur eine Liste an Ausdruecken mit Operatoren dazwischen. Sobald alles geparsed wurde, sind Benutzerdefinierte Operatoren samt Prioritaeten bekannt und die Operator Sequence wird in einem zweiten Parserschritt nach Prioritaet und Assiziativitaet korrekt zerlegt. Danach gehts ganz normal weiter mit semantischer Analyse. Was haeltst du von diesem Ansatz? 🤡

    Interessant, vorrangig deswegen, weil ich diesen Ansatz bisher nicht im entferntesten mit selbstdefinierten Operatoren gesehen habe...!? Wenngleich das ganze einen zweiten Durchlauf für die semantische Analyse fordert... sofern Du eine typsichere Sprache hast...?!

    Ich denke gerade an einen Ausdruck wie

    int a = <"selbstdefinierter Operator"> + 1;
    

    Wie erkennst Du, dass es sich um einen selbstdefinierten Operator handelt, wenn dieser noch nicht deklariert wurde? An der Stelle weiß ich ja nicht, ob ["selbstdefinierter Operator"] überhaupt ein Integer addiert werden kann und wenn ja, ob da ein einem Integer zuweisbares Objekt rauskommt.
    Merkst Du Dir diese Stellen, um sie nachträglich zu prüfen?

    ---
    @Moderator mit Rechten: ggfs. abtrennen



  • Mit dynamischen Operatoren meinte ich selbstdefinierte, richtig. Zunaechst einmal ist es so, dass man zwischen selbst definierten Operatoren und selbst definierten Overloads unterscheiden muss. Bevor ein Operator ueberladen werden kann, muss er erstmal definiert sein. Da gibt es natuerlich die built-in Definitionen, aber man kann sich auch seine eigenen bauen (Syntax aendert sich gefuehlt jede Woche mal):

    // neuer Operator $$
    // Symbol $$, binaer mit Prioritaet 42, links-assoziativ
    operator $$ binary(42, left);
    

    Sobald man den definiert hat, kann man diesen ganz normal ueberladen, wie man es in C++ auch tun wuerde.
    Eine symantische Analyse ist damit nicht wirklich erforderlich, da es keine semantisch "falschen" Operator Definitionen gibt. Die beiden Parserschritte koennen daher direkt hintereinander ausgefuehrt werden. Das einige, was geprueft werden muss, ist dass es nicht mehrere wiederspruechige Definitionen gibt. Man kann einen Operator durchaus als binaer und unaeren prefix/postfix gleichzeitig definieren, allerdings niemals alle 3, da es dann zu Mehrdeutigkeiten kommen koennte.

    Zu deinem Beispiel: Es wird beim aufbauen des ASTs zunaechst angenommen, dass es ein gueltiger Operator ist und der OpSeq Knoten erstellt. Sobald alles geparsed wurde, habe ich eine Liste aller definierten Operatoren sowie eine Liste aller OpSeqs vorliegen und kann im zweiten Parseschritt - dem Zerlegen aller OpSeqs - nachsehen, ob der Operator tatsaechlich existiert. Ob dieser Operator-Overload dann fuer die gegebenen Argument-Typen existiert, ist eine ganz andere Frage und wird erst bei der semantischen Analyse geprueft.



  • Kellerautomat schrieb:

    Zunaechst einmal ist es so, dass man zwischen selbst definierten Operatoren und selbst definierten Overloads unterscheiden muss.

    Finde ich gut.

    Kellerautomat schrieb:

    // neuer Operator $$
    // Symbol $$, binaer mit Prioritaet 42, links-assoziativ
    operator $$ binary(42, left);
    

    Sobald man den definiert hat, kann man diesen ganz normal ueberladen, wie man es in C++ auch tun wuerde.

    Ich habe noch keine dynamischen Operatoren, ich tue mich derzeit an zwei Dingen schwer: der Magic-Number "42" - wobei mir auch noch nichts besseres eingefallen ist - und "left". Ich bin sowieso schon leicht inflationär was Schlüsselwörter angeht. Selbiges mit binary.
    Wie sieht's mit ternärer Operatoren aus?

    Kellerautomat schrieb:

    Die beiden Parserschritte koennen daher direkt hintereinander ausgefuehrt werden. Das einige, was geprueft werden muss, ist dass es nicht mehrere wiederspruechige Definitionen gibt. Man kann einen Operator durchaus als binaer und unaeren prefix/postfix gleichzeitig definieren, allerdings niemals alle 3, da es dann zu Mehrdeutigkeiten kommen koennte.

    Präfix-Operatoren gibt's bei mir nicht mehr - mit Ausnahme von 'negativ'. Ich hoffe, das bleibt auch so. 🙂

    Kellerautomat schrieb:

    Zu deinem Beispiel: Es wird beim aufbauen des ASTs zunaechst angenommen, dass es ein gueltiger Operator ist und der OpSeq Knoten erstellt. Sobald alles geparsed wurde, habe ich eine Liste aller definierten Operatoren sowie eine Liste aller OpSeqs vorliegen und kann im zweiten Parseschritt - dem Zerlegen aller OpSeqs - nachsehen, ob der Operator tatsaechlich existiert. Ob dieser Operator-Overload dann fuer die gegebenen Argument-Typen existiert, ist eine ganz andere Frage und wird erst bei der semantischen Analyse geprueft.

    Aber bringt das einen erwähnenswerten Vorteil? Du musst den Operator ja sowieso deklarieren, da könnte man die Overloads ja als Deklaration auch gleich verlangen und sich einen Durchgang sparen.
    Grundsätzlich finde ich die Idee das Parsen so aufzuteilen aber sehr interessant. Ich denke da aber mehr an Präprozessing.



  • Xin schrieb:

    Kellerautomat schrieb:

    // neuer Operator $$
    // Symbol $$, binaer mit Prioritaet 42, links-assoziativ
    operator $$ binary(42, left);
    

    Sobald man den definiert hat, kann man diesen ganz normal ueberladen, wie man es in C++ auch tun wuerde.

    Ich habe noch keine dynamischen Operatoren, ich tue mich derzeit an zwei Dingen schwer: der Magic-Number "42" - wobei mir auch noch nichts besseres eingefallen ist - und "left". Ich bin sowieso schon leicht inflationär was Schlüsselwörter angeht. Selbiges mit binary.

    Ich gebe dir Recht. Ein Grund, warum ich meine Syntax dafuer dauernd aendere, ist weil ich immer versuche die perfekte Syntax zu bekommen. Ich habe sie noch nicht gefunden.

    Xin schrieb:

    Wie sieht's mit ternärer Operatoren aus?

    Ich habe darueber nachgedacht, aber ich halte den praktischen Nutzen fuer zu gering. Wenn, dann wuerde ich Operatoren so generalisieren, dass man eine beliebige Aritaet haben kann, aber ich bin mir nicht mal sicher, wie man das machen koennte. Das Potential fuer Mehrdeutigkeiten ist riesig, und ich koennte mir vorstellen, dass die Regeln ziemlich kompliziert werden. Falls du irgendwo Literatur dazu hast, immer her damit, das Thema interessiert mich.
    Und was ist eigentlich mit einer variablen Anzahl an Argumenten? 😃

    Xin schrieb:

    Kellerautomat schrieb:

    Die beiden Parserschritte koennen daher direkt hintereinander ausgefuehrt werden. Das einige, was geprueft werden muss, ist dass es nicht mehrere wiederspruechige Definitionen gibt. Man kann einen Operator durchaus als binaer und unaeren prefix/postfix gleichzeitig definieren, allerdings niemals alle 3, da es dann zu Mehrdeutigkeiten kommen koennte.

    Präfix-Operatoren gibt's bei mir nicht mehr - mit Ausnahme von 'negativ'. Ich hoffe, das bleibt auch so. 🙂

    Was hast du gegen Praefix-Operatoren? Sollten diese nicht sogar einfacher zu parsen sein als Postfix?

    Xin schrieb:

    Kellerautomat schrieb:

    Zu deinem Beispiel: Es wird beim aufbauen des ASTs zunaechst angenommen, dass es ein gueltiger Operator ist und der OpSeq Knoten erstellt. Sobald alles geparsed wurde, habe ich eine Liste aller definierten Operatoren sowie eine Liste aller OpSeqs vorliegen und kann im zweiten Parseschritt - dem Zerlegen aller OpSeqs - nachsehen, ob der Operator tatsaechlich existiert. Ob dieser Operator-Overload dann fuer die gegebenen Argument-Typen existiert, ist eine ganz andere Frage und wird erst bei der semantischen Analyse geprueft.

    Aber bringt das einen erwähnenswerten Vorteil? Du musst den Operator ja sowieso deklarieren, da könnte man die Overloads ja als Deklaration auch gleich verlangen und sich einen Durchgang sparen.

    Ich finde es sauberer abzutrennen, aber es sollte kein Problem darstellen, einen syntaktischen Shortcut einzufuehren. Ist ja nur Syntaxzucker.

    Xin schrieb:

    Grundsätzlich finde ich die Idee das Parsen so aufzuteilen aber sehr interessant. Ich denke da aber mehr an Präprozessing.

    Danke, ich hab mir den Ansatz selbst ausgedacht. 😃
    Aber was meinst du mit Praeprozessing?



  • [quote="Kellerautomat"]Wenn, dann wuerde ich Operatoren so generalisieren, dass man eine beliebige Aritaet haben kann, aber ich bin mir nicht mal sicher, wie man das machen koennte.quote]

    Meinst du sowas?

    ($$ a b c d)


  • Mod

    @Moderator mit Rechten: ggfs. abtrennen

    Hihi, mein erster Split! Hoffe ich habe nicht zu viel Verwirrung gestiftet.

    Was hast du gegen Praefix-Operatoren? Sollten diese nicht sogar einfacher zu parsen sein als Postfix?

    Vielleicht meint er sowas wie a+++b . Entspricht es (a++)+b oder a+(++b) ? Wenn du aber lediglich den Postfix-Operator definierst verschwindet auch die Ambiguität.



  • Mechanics schrieb:

    Kellerautomat schrieb:

    Wenn, dann wuerde ich Operatoren so generalisieren, dass man eine beliebige Aritaet haben kann, aber ich bin mir nicht mal sicher, wie man das machen koennte.

    Meinst du sowas?

    ($$ a b c d)

    int operator $$ ## ?? (int a, int b, int c, int d)
    {
        return a + b + c + d;
    }
    
    int x = 42 $$ 666 ## 1337 ?? 9001;
    

    Arcoth schrieb:

    Kellerautomat schrieb:

    Was hast du gegen Praefix-Operatoren? Sollten diese nicht sogar einfacher zu parsen sein als Postfix?

    Vielleicht meint er sowas wie a+++b . Entspricht es (a++)+b oder a+(++b) ? Wenn du aber lediglich den Postfix-Operator definierst verschwindet auch die Ambiguität.

    Das habe ich ja beretis gesagt: Es darf nur maximal 2 aus {unary prefix, unary postfix, binary} geben, damit es eindeutig ist.
    Auf die Regel bin ich selbst draufgekommen, aber aehnliche regeln fuer n-ary Operatoren aufstellen kann ich schwer.

    Von formalen Beweisen und der ganzen Theorie hinter Parsing halte ich eh nix, ich bin da praxisorientiert. Wenn ich fuer etwas nicht sinnvolle Regeln aufstellen kann, wie sollen dann erst die Benutzer meine Sprache das Feature einsetzen koennen?

    Edit: @Arcoth: Im uebrigen waere die Bedeutung deines Codes in meiner Sprache a +++ b, wobei +++ ein einziger Operator ist. Wer das getrennt haben will, muss Whitespace einfuegen.



  • Kellerautomat schrieb:

    int operator $$ ## ?? (int a, int b, int c, int d)
    {
        return a + b + c + d;
    }
    
    int x = 42 $$ 666 ## 1337 ?? 9001;
    

    Mir ist jetzt nicht wirklich klar, was das ausdrücken soll.



  • Ein Quartaerer Operator halt.



  • Gibt es sowas in freier Wildbahn? Es ist ja schon schwer, sinnvolle Beispiele für ternäre Operatoren, die Funktionen sind (also nicht ?: ) zu finden.



  • Koennte man z.B. statt Expression Templates zur Optimierung verwenden.



  • Bashar schrieb:

    Gibt es sowas in freier Wildbahn? Es ist ja schon schwer, sinnvolle Beispiele für ternäre Operatoren, die Funktionen sind (also nicht ?: ) zu finden.

    Pythons slice operator wäre ein quartärer Operator:

    a[start:end:step]
    


  • Kellerautomat schrieb:

    Ich gebe dir Recht. Ein Grund, warum ich meine Syntax dafuer dauernd aendere, ist weil ich immer versuche die perfekte Syntax zu bekommen. Ich habe sie noch nicht gefunden.

    Ich frickel jetzt seit 10 Jahren an der Syntax, es kommen laufend neue Ideen, aber der Kern ist inzwischen eigentlich recht fix.
    Wichtig ist nur, dass man nicht in vorgegebenen Mustern denkt.

    Kellerautomat schrieb:

    Was hast du gegen Praefix-Operatoren? Sollten diese nicht sogar einfacher zu parsen sein als Postfix?

    Weil es - für mich logischer ist - ein Objekt zu nehmen und dann etwas damit zu machen. Es heißt ja auch nicht method.member<-this(arg1, arg2). oder (arg1, arg2)func. Warum sollte ein Operator also vorne stehen?

    Kellerautomat schrieb:

    Ist ja nur Syntaxzucker.

    Alles ist nur Syntax-Zucker, wenn man nicht in Assembler programmiert. (Und gefälligst ohne Makro-Assembler - wir haben mal eine Grafik-Adventure-Engine in Assembler geschrieben, nur sah man gar nicht, dass es Assembler war... ein guter Assembler kann auch zuckersüß sein)

    Xin schrieb:

    Aber was meinst du mit Praeprozessing?

    #if, #else, #endif

    Und den Text dazwischen muss man sich ja auch irgendwie merken.

    Arcoth schrieb:

    @Moderator mit Rechten: ggfs. abtrennen

    Hihi, mein erster Split! Hoffe ich habe nicht zu viel Verwirrung gestiftet.

    Klappt doch wunderbar. 🙂

    Arcoth schrieb:

    Was hast du gegen Praefix-Operatoren? Sollten diese nicht sogar einfacher zu parsen sein als Postfix?

    Vielleicht meint er sowas wie a+++b . Entspricht es (a++)+b oder a+(++b) ? Wenn du aber lediglich den Postfix-Operator definierst verschwindet auch die Ambiguität.

    👍

    Ich habe deutlich mehr Postfix-Operatoren. Ich würde mir mit quasi allem in die Quere kommen.

    Kellerautomat schrieb:

    Mechanics schrieb:

    Meinst du sowas?

    ($$ a b c d)

    int operator $$ ## ?? (int a, int b, int c, int d)
    {
        return a + b + c + d;
    }
    
    int x = 42 $$ 666 ## 1337 ?? 9001;
    

    Denkmuster aufbrechen... eine Möglichkeit:

    int operator (int a, "$$", int b, "##", int c, "??", int d );
    

    Ein String in einer Argumentenliste kann kein Argument sein. Also muss er irgendwas anderes bedeuten.

    Kellerautomat schrieb:

    Von formalen Beweisen und der ganzen Theorie hinter Parsing halte ich eh nix, ich bin da praxisorientiert.

    Grundsätzlich unterschreibe ich das auch so... Top-Down, Bottom-Up, Single-Pass, alles gut und schön: Die Sprache muss gut zu programmieren sein, der Rest ist heute das Problem des Parser-Herstellers.
    Trotzdem kann es nicht schaden, sich mit der Theorie ein wenig auseinander zu setzen.

    Bashar schrieb:

    Kellerautomat schrieb:

    Ein Quartaerer Operator halt.

    Gibt es sowas in freier Wildbahn? Es ist ja schon schwer, sinnvolle Beispiele für ternäre Operatoren, die Funktionen sind (also nicht ?: ) zu finden.

    Ich habe vor 10 Jahren eine Umfrage gestartet, was in aktuellen Programmiersprachen fehlt. Die wichtigste Erkenntnis ist, dass Software-Entwickler es sehr schwer haben, Dinge zu vereinfachen, weil sie es gewohnt sind, Probleme mit den Mitteln zu lösen, die sie haben.
    Mathematiker, Ingenieure oder Anfänger interessieren sich nicht dafür, dass die Mittel begrenzt sind. Die Sachen sich, da muss es bestimmt ein Werkzeug für geben und wenn nicht, beschwere ich mich. Der Informatiker weiß, es gibt kein Werkzeug und löst sein Problem so.
    Die Leute, die Compiler bauen, machen das meistens aus Spaß. Manche bauen auch das Werkzeug, dass ihnen fehlt.
    Dass manche Dinge nicht in freier Wildbahn auftauchen liegt oftmals einfach nur daran, dass sich kein Sprachdesigner gefunden hat, der sich fragte, ob es Leute geben könnte, die sowas brauchen könnten, bzw. die sowas praktisch finden. Wer braucht schon ternäre Operatoren? Man kann einen Bruch wie 1 1/2 ja auch in 3/2 schreiben. Oder muss halt den passenden Konstruktor der Klasse Fraction( 1, 1, 2 ) aufrufen. Oder man definiert sich einen Operator 1_1_2, wenn man viel mit Brüchen rechnet, der Fraction( 1, 1, 2 ) aufruft.



  • Arcoth schrieb:

    Vielleicht meint er sowas wie a+++b . Entspricht es (a++)+b oder a+(++b) ? Wenn du aber lediglich den Postfix-Operator definierst verschwindet auch die Ambiguität.

    Wo ist da eine Ambiguität? a+++b ist genauso wie a+++++++++b in C++ genau definiert. In K11ts Sprache leicht anders, aber auch wohldefiniert.

    Xin schrieb:

    Kellerautomat schrieb:

    Was hast du gegen Praefix-Operatoren? Sollten diese nicht sogar einfacher zu parsen sein als Postfix?

    Weil es - für mich logischer ist - ein Objekt zu nehmen und dann etwas damit zu machen. Es heißt ja auch nicht method.member<-this(arg1, arg2). oder (arg1, arg2)func. Warum sollte ein Operator also vorne stehen?

    Verstehe ich nicht. In C++ hast du sowas perverses* wie next(begin(v)) , aber was eigentlich gemeint ist ist "nimm v, nehme davon den Startiterator und inkrementiere diesen". Eine sprechendere Schreibweise wäre z.B. v:begin():next() .

    Also nein, func(arg1, arg2) ist Präfix-Notation. Sogar das meiste aus C++ ist Präfix-Notation. Aber Postfix-Notation wäre lesbarer.

    *im wahrsten Wortsinn


Anmelden zum Antworten