DAS KANN MAN SCHON ERNSTER NEHMEN: BITTE UM BUGBERICHTE, FEEDBACK, LOB, ..., denn das war viel Arbeit
*** KAPITEL 9: TEXT ***
9. Wesentliche Erweiterung des mathematischen Parsers
9.1. Hinzufügen neuer Operatoren: Vergleiche, boolesche Operatoren wie < <= <> = >= > and or xor
9.1.1. Die neue Präzedenztabelle. Eine Diskussion
In diesem Abschnitt möchte ich über die Präzedenz der
Logischen Operatoren sprechen:
- logische Vergleiche (engl. relational operators oder kurz RelOps): =, <, <=, <>, >=, >
- logische boolesche Verknüpfung: and, or, xor
Von unserer Entscheidung hier hängt viel von dem ab, was man das "look and feel" einer Programmiersprache nennt.
Zur Erinnerung unsere
bisherige Toy-C-Präzedenztabelle:
Code: Alles auswählen
Prozedur Operator Präzedenz
-----------------------------------------------------------
ValueFactor() ( ... ) höchste
-----------------------------------------------------------
ValueFactor() Negation
bzw. Unäres -|+
-----------------------------------------------------------
MulTerm() * / %
-----------------------------------------------------------
AddExpression() + - niedrigste
-----------------------------------------------------------
Vergleichen wir damit eine (ich habe sie vereinfacht) beliebige
Pascal-Präzedenztabelle:
Code: Alles auswählen
Prozedur Operator Präzedenz
-----------------------------------------------------------
ValueFactor() ( ... ) höchste
-----------------------------------------------------------
ValueFactor() Negation
bzw. Unäres -|+
Unäres not
-----------------------------------------------------------
MulTerm() * / and
-----------------------------------------------------------
AddExpression() + - or
-----------------------------------------------------------
Relation() = < <= <> >= > niedrigste
-----------------------------------------------------------
(Quelle:
http://www.freepascal.org/docs-html/ref/refch12.html)
Wir finden "
not", "
and", "
or" und "
= < <= <> >= >" neu in dieser Tabelle.
Anmerkung: Ich möchte NICHT die Unterscheidung von C zwischen "=" und "==" übernehmen. Obwohl Toy-C genannt, ist meine Sprache hier näher an Pure Basic als an C. Auch were ich "and" und nicht "&&" und auch "or" verwenden.
Überlegen wir uns, wie mit der Grammatik von Pascal folgende Rechnung gelöst werden würde:
Code: Alles auswählen
if ( a=2 and b<>3 ) <=== Rechnung
if ( a=(2 and b)<>3 ) <=== würde so gelöst
Und das gefällt mir nicht!
Warum ist das so?
Es liegt daran, dass "
and" in der Tabelle
höherwertiger ist
als "
=", also wird
"and" vor "=" ausgerechnet, genauso wie eben "* / %" vor "+ -".
Ich schlage folgende
vorläufige Präzedenztabelle für Toy-C vor:
Code: Alles auswählen
Prozedur Operator Präzedenz
-----------------------------------------------------------
ValueFactor() ( ... ) höchste
-----------------------------------------------------------
ValueFactor() Negation
bzw. Unäres -|+
-----------------------------------------------------------
MulTerm() * / %
-----------------------------------------------------------
AddExpression() + -
-----------------------------------------------------------
Relation() < <= <> = >= >
-----------------------------------------------------------
BoolAnd() and
-----------------------------------------------------------
BoolOrXor() or xor niedrigste
-----------------------------------------------------------
Als
Vorlage habe ich eine
C-Präzedenztabelle gewählt (siehe:
http://montcs.bloomu.edu/~bobmon/Inform ... scal.shtml) und für meine Zwecke vereinfacht.
Wenn wir jetzt nochmals die
Beispiele von oben vergleichen, dann ergibt sich folgende
Operatorenreihenfolge:
Code: Alles auswählen
if ( 3*4 and 3+5 )
if ( (3*4) and (3+5) ) <=== würde so gelöst
-----------------------------------------------
if ( x<=4 and y>5 )
if ( (x<=4) and (y>5) ) <=== würde so gelöst
Und das ist für mich schon intuitiver.
Zur Vollständigkeit schauen wir uns noch die
Tabelle von Pure Basic (siehe PB-Hilfe -> Variablen, Typen, Operatoren) an, die recht ähnlich ist (bis auf 'not', siehe später):
Code: Alles auswählen
-----------------+---------------------
Prioritäts-Level | Operatoren
-----------------+---------------------
8 (hoch) | ~, -
7 | <<, >>, %, !
6 | |, &
5 | *, /
4 | +, -
3 | >, >=, <, <=, =, <>
2 | Not
1 (niedrig) | And, Or, XOr
-----------------+---------------------
Das war jetzt einfach Strg-C, Strg-V.
Diese Diskussion über Präzedenzen findet sich auch in Crenshaw und ist sehr interessant, aber leider auf Englisch (
http://www.pp4s.co.uk/main/tu-trans-comp-jc-06.html).
9.1.2. Mathematischer Parser, Erweiterung um die Operatoren "< <= <> = >= > and or xor"
Die Erweiterung um diese Operatoren geschieht genau so, wie wir uns das vorstellen.
Jede Präzedenzebene bekommt wieder eine Prozedur zugewiesen.
Hier ein Ausschnitt aus dem
Declare-Teil von Modul
Parser():
Code: Alles auswählen
; --- Mathematischer Parser ---
Declare ValueFactor() ; holt einen Operanden-Wert, ( ... ), unary -|+
Declare MulTerm() ; bearbeitet Mul-Ops: *, /, %
Declare AddExpression() ; bearbeitet Add-Ops: +, -
Declare Relation() ; logische Vergleiche <, <=, <>, =, >=, >
Declare BoolAnd() ; boolesches And
Declare BoolOrXor() ; boolesches Or, Xor
Declare Expression() ; mathematischer Expression Parser 3+4*(4+3)
Wie immer ruft
Expression() die
niedrigste Präzedenz-Stufe auf, jetzt
BoolOrXor() und nicht mehr AddExpression().
Die Prozeduren finden wir im
Gesamt-Code, den ich
am Ende des Kapitels präsentiere.
9.2. Umbau der Implementation des Unary -|+
Wenn man ein System hat, das man einsetzt, wie hier z.B. eben die Rekursion, dann
sollte man dieses System auch konsequent durchziehen. Das sollten wir uns merken.
Während unsere
erste "naive" Version des unären + bzw. des unären -
bestens und sogar perfekt funktioniert, ist sie eben
nicht voll an unser System angepasst.
Bauen wir also unsere
Präzedenztabelle nochmals um (und wir werden es noch mehrmals tun

):
Code: Alles auswählen
Prozedur Operator Präzedenz
-----------------------------------------------------------
ValueFactor() ( ... ) höchste
-----------------------------------------------------------
NegValueFactor() Negation
bzw. Unäres -|+
-----------------------------------------------------------
MulTerm() * / %
-----------------------------------------------------------
AddExpression() + -
-----------------------------------------------------------
Relation() < <= <> = >= >
-----------------------------------------------------------
BoolAnd() and
-----------------------------------------------------------
BoolOrXor() or xor niedrigste
-----------------------------------------------------------
Wir fügen in unsere Prozedur-Aufrufkette einfach
NegValueFactor() ein:
Code: Alles auswählen
Procedure NegValueFactor()
; --> Token/Lexem steht auf Value ODER
; --> auf unary + | unary -
; Ist ein 'unary -' vor ValueFactor?
If Scanner::Token='-'
Scanner::GetToken()
ValueFactor() ; Aufstieg zu ValueFactor()
Code::_Neg()
; Ist ein 'unary +' vor ValueFactor?
ElseIf Scanner::Token='+'
Scanner::GetToken()
ValueFactor() ; Aufstieg zu ValueFactor()
; 'normaler' ValueFactor
Else
ValueFactor() ; Aufstieg zu ValueFactor()
EndIf
; --> in Token/Lexem ist Token/Lexem nach Value
; --> wenn die Expression weitergeht, ist das ein Operator
; --> Abstieg zu MulTerm()
EndProcedure
In Wirklichkeit ist das eine ganz einfache und kurze Prozedur, sie wird nur durch meine Kommentare so lang.
Was passiert hier?
Besprechen wir nur den Teil von "unary -", der andere ist identisch:
- Scanner::GetToken(): Bei einem "unary -" holt der Parser das nächste Token-Lexem-Pärchen
- ValueFactor(): ruft ValueFactor() auf und kümmert sich um Values
- Code::_Neg(): kommt von ValueFactor() zurück und hat sich quasi "gemerkt", dass er danach noch negieren muss
Interessant ist, dass diese Ausführung im Prinzip
EXAKT dasselbe ist
wie unser naiverer Ansatz mit dem
Flag negate:
"merken", ob "unary -" war ---> Value holen ---> "neg" ausführen, weil "gemerkt"
Die
alte Prozedure ValueFactor() (ohne Kommentare oben/unten):
Code: Alles auswählen
Procedure ValueFactor()
; pruefe auf Vorzeichen (unary - | unary +)
; Ja? Überspringe es und setze bei "-" ein Flag
If Scanner::Token = '-': Scanner::GetToken(): negate = #True
ElseIf Scanner::Token = '+': Scanner::GetToken()
EndIf
; je Token-Art: Ausgabe des Values als ASM-Code
Select Scanner::Token
Case '#' : Code::PushConst(Scanner::Lexem)
Case 'x' : Code::PushGlobal(Scanner::Lexem)
Case '(' : Scanner::GetToken() ; überspringe "("
Expression() ; ")" wird weiter unten übersprungen
Default : Scanner::Expected("Korrekter Operand (Konstante Zahl, Variablen- oder Prozedurname)")
EndSelect
; Teste Flag "negate"
; Ja? Führe "unary -" durch: negiere Value
If negate = #True
Code::_Neg()
EndIf
; holt nächstes Token (=Operator)
; überspringe eventuell ")"
Scanner::GetToken()
EndProcedure
Die
neue Prozedure ValueFactor() (ohne Kommentare oben/unten):
Code: Alles auswählen
Procedure ValueFactor()
; je Token-Art: Ausgabe des Values als ASM-Code
Select Scanner::Token
Case '#' : Code::PushConst(Scanner::Lexem)
Case 'x' : Code::PushGlobal(Scanner::Lexem)
Case '(' : Scanner::GetToken() ; überspringe "("
Expression() ; ")" wird weiter unten übersprungen
Default : Scanner::Expected("Korrekter Operand (Konstante Zahl, Variablen- oder Prozedurname)")
EndSelect
; holt nächstes Token (=Operator)
; überspringe eventuell ")"
Scanner::GetToken()
EndProcedure
Der Test nach "unary -|+" ist jetzt in NegValueFactor() ausgelagert.
Durch die Aufruflogik der Prozeduren
ersparen wir uns auch das
Flag negate. Wir kommen ja bereits vom erfolgreichen Test nach "unary -" in ValueFactor() an, also können wir bei Rückkehr automatisch "neg" ausführen.
Natürlich - wir sehen es dann in der Gesamtimplementation im Code-Teil - müssen wir auch wieder einen
Declare-Teil und die
Aufrufe von MultTerm() von ValueFactor() auf NegValueFactor()
ändern, das können wir aber mittlerweile blind (hoffe ich).
9.3. Hinzufügen eines neuen Operators: Potenzieren ^
9.3.1. Erster naiver und falscher Ansatz
Auf jeden Fall fügen wir die Potenz (engl exponentiation oder '2 power to 4') mit einer
möglichst hohen Präzedenz ein, wie das auch die Programmiersprache Python tut (siehe wieder einmal:
http://montcs.bloomu.edu/~bobmon/Inform ... scal.shtml). Die anderen Sprachen erwähne ich gar nicht, denn diese besitzen (wie PB auch!) gar keinen Exponentialoperator.
Wir werden das
"^"-Zeichen (Zirkumflex oder engl. caret) verwenden (nebenbei: oben erwähntes Python verwendet übrigens "**").
Präzedenztabelle für die
Potenz:
Code: Alles auswählen
Prozedur Operator Präzedenz
-----------------------------------------------------------
ValueFactor() ( ... ) höchste
-----------------------------------------------------------
NegValueFactor() Negation
bzw. Unäres -|+
-----------------------------------------------------------
Exponentiation() ^
-----------------------------------------------------------
MulTerm() * / %
-----------------------------------------------------------
AddExpression() + -
-----------------------------------------------------------
Relation() < <= <> = >= >
-----------------------------------------------------------
BoolAnd() and
-----------------------------------------------------------
BoolOrXor() or xor niedrigste
-----------------------------------------------------------
Eingefügt haben wir "
^"
zwischen MulTerm() und NegValueFactor().
Die erste
falsche Prozedur Exponentiation():
Code: Alles auswählen
Procedure Exponentiation_LA() ; *** falsch: links_assoziativ ***
; kann entfernt werden, wenn nicht benötigt
; --> Token/Lexem steht auf Value
; Aufstieg zu NegValueFactor()
NegValueFactor()
; Power-Op in Token/Lexem?
While Scanner::Token='^'
; vor Aufstieg nächstes Token (=Value) holen
Scanner::GetToken()
; Aufstieg zu NegValueFactor()
; ====> FALSCH! NUR BEI SELBSTAUFRUF RICHTIG RECHTSASSOZIATIV !!! <====
; ====> siehe richtige Lösung weiter unten
NegValueFactor()
; Ausgabe des ASM-Codes des Operators
Code::_Exp()
Wend
; --> in Token/Lexem ist Token/Lexem nach dem Operator ODER
; --> in Token/Lexem ist Token/Lexem nach dem Value
; --> Abstieg zu MulTerm
EndProcedure
Lassen wir jetzt einmal den Parser mit folgendem
Source-Code laufen.
Code: Alles auswählen
-2^-4
XXXXXXXXXXXXXXX
2^3
XXXXXXXXXXXXXXX
2^3*3
XXXXXXXXXXXXXXX
2^3^4 // <==== HIER LIEGT EIN PROBLEM VOR
Unser Compiler macht folgendes
.ta-File daraus:
Code: Alles auswählen
push Const 2
neg
push Const 4
neg
exp <=== PASST
push Global XXXXXXXXXXXXXXX
push Const 2
push Const 3
exp <=== PASST
push Global XXXXXXXXXXXXXXX
push Const 2
push Const 3
exp <=== PASST: "^" höhere Präzedenz als "*"
push Const 3
mul
push Global XXXXXXXXXXXXXXX
push Const 2 <=== !!!!!! FALSCH FALSCH FALSCH !!!!!!
push Const 3
exp
push Const 4
exp
Warum passt das nicht? Was verflixt nochmal soll daran jetzt bitte falsch sein?
Es hat doch auch z.B. bei "+,-" bzw. "*,/,%" problemlos funktioniert.
9.3.2. Operatorassoziativität und der Vorschlag einer Lösung
Beantworten wir die oben gestellte Frage.
So wie wir die falsche Prozedur Exponentiation() programmiert haben,
löst der Compiler das Ganze
folgendermaßen, also genauso wie 2+3+4 oder 2*3*4 usw.:
Dass er das genauso löste wie 2+3+4 oder 2*3*4 ist auch kein Wunder, ist die Prozedur doch wieder identisch mit denen der anderen Operatoren, also ein leicht varierter (nur bei den Operatoren und den Aufrufen) Klon.
Er rechnet also
zuerst 2^3 aus und das
Ergebnis nimmt er dann
^4. Bei der Multiplikation, der Addition, bei nahezu allen anderen Operatoren würde das stimmen.
Das Phänomen, das wir gerade kennen lernen und das uns jetzt ärgert, nennt man
Operatorassoziativität.
(siehe:
http://de.wikipedia.org/wiki/Operatoras ... vit%C3%A4t)
Die
meisten Operatoren sind
linksassoziativ, d.h. sie werden
von links nach rechts gelöst.
Der
Exponentialoperator ist aber
rechtsassoziativ, d.h. wir müssen ihn
von rechts nach links lösen (siehe:
http://de.wikipedia.org/wiki/Operatorra ... Operatoren).
Auch in den andauernd von mir zitierten Präzendenztabellen (diese da:
http://montcs.bloomu.edu/~bobmon/Inform ... scal.shtml) findet sich eine Spalte, die wir bisher wahrscheinlich noch gar nicht bewusst wahrgenommen haben: Sie heißt "Associativity", also Assoziativität.
Ich möchte diese Spalte in den nächsten Präzedenztabellen von Toy-C ebenfalls aufnehmen.
In der
Literatur über Compiler habe ich
über die Exponentation folgende Aussagen gefunden:
- Die Exponentation sei wegen ihrer Rechtsassoziativität sehr schwer umzusetzen.
- Die meisten Programmiersprachen lösen das Problem, indem sie einfach keinen Exponentialoperator einfügen, auch Pure Basic - siehe PB-Hilfe unter Pow() -, Pascal und alle C-Abarten machen das, wenn wir in die Tabelle schauen. Sie nutzen eine Prozedur, die die Exponentialfunktion umsetzt.
Damit umschifft man das Problem, weil eine Prozedur aufgerufen in einer Prozedur automatisch rechtsassoziativ wird.
Ein Beispiel in Pure Basic:
Code: Alles auswählen
; =================
; Angabe ist: 2^2^3
; =================
RA = Pow(2,Pow(2,3)) ; richtig rechtsassoziativ : 2^(2^3) ==> 256
LA = Pow(Pow(2,2),3) ; falsch linksassoziativ : (2^2)^3 ==> 64
Debug "richtig rechtsassoziativ : "+Str(RA)
Debug "falsch linksassoziativ : "+Str(LA)
Richtig ergibt das 256. Laut Wikipedia und Assoziativgesetz (http://de.wikipedia.org/wiki/Assoziativ ... Einordnung: gleich oberhalb der Überschrift "Einordnung") ist das die richtige Lösung.
Würde der Potenzoperator linksassoziativ sein, dann wäre die Lösung 64.
Aufgrund der Aussagen in der Literatur und auch deshalb, weil das Problem auch in Pure Basic mit einer Prozedur gelöst ist,
fasste ich den Plan, den Exponentialoperator wegzulassen und später eine
Funktion dafür bereitzustellen.
Immerhin macht das
C über C++ bis C# mit einer Prozedur bzw. Methode "pow()", auch
Pascal kennt diese
Prozedur, einzig
Python und
Lua (
http://www.lua.org/pil/3.5.html) machen das unter den mir bekannten Programmiersprachen
mit einem eigenen Operator, selbstverständlich mit korrekter Rechtsassoziativität (wieder siehe die schon unzählige Male verlinkte Präzedenztabelle bzw. Lua-Link vorhin).
Also so richtige Schwergewichte, also die Big Boys, "schwindeln" sich mit einer Prozedur aus der Assoziativitätsproblematik, nur Python nicht, also müsste es ja einen Weg geben.
Nach relativ kurzer Überlegung fand ich eine Lösung. Und diese ist so derart einfach wie sexy, dass ich - ich habe oben von einem Vorschlag gesprochen - gar nicht glauben kann, dass sie 1. funktioniert und dass 2. ich sie entdeckt habe.
Aus diesem Grund bitte ich alle, den
Parser in allen erdenklichen Lebenslagen zu testen und zu schauen,
ob meine Lösung letztlich auch wasserdicht funktioniert (vor allem auch INNERHALB komplizierter mathematischer Ausdrücke mit Klammern und Negationen uvm.).
Ich danke allen Testern aus tiefstem Herzen bereits im Voraus.
Sollten sich Bugs finden, was solls. Die recht häufige Prozedur-Variante können wir immer noch ohne viel Aufwand umsetzen.
Ein
Source-Code-Beispiel:
Code: Alles auswählen
2+3+4 // linksassoziativ: (2+3)+4
XXXXXXXXXXXX
2^2^3 // rechtsassoziativ: 2^(2^3)
DAS_GLEICHE
2^(2^3) // ASM-Code sollte gleich sein: 2^(2^3)
FUNKTIONSTEST_1
25*(2^3^4+(25+3)/2) /* testet, ob das Ganze auch eingefügt in einen
* komplizierteren Ausdruck aus der Rekursion herausfindet.
*/
FUNKTIONSTEST_2
25+(2^-3^-(4+5*2) *100) /* 2. Exponent ein Klammerausdruck
* und einige unnötige Negationen
*/
Die richtige Variante der Prozedur Exponentation() erzeugt bei mir folgende
VM-ASM-Ausgabe (Leerzeilen und Kommentare nachträglich von mir eingefügt):
Code: Alles auswählen
push Const 2 <=== 2+3+4 // linksassoziativ: (2+3)+4
push Const 3
add
push Const 4
add
push Global XXXXXXXXXXXX
push Const 2 <=== 2^2^3 // rechtsassoziativ: 2^(2^3)
push Const 2
push Const 3
exp
exp
push Global DAS_GLEICHE
push Const 2 <=== 2^(2^3) // ASM-Code sollte gleich sein: 2^(2^3)
push Const 2
push Const 3
exp
exp
push Global FUNKTIONSTEST_1 <=== 25*(2^3^4+(25+3)/2)
push Const 25
push Const 2
push Const 3
push Const 4
exp
exp
push Const 25
push Const 3
add
push Const 2
div
add
mul
push Global FUNKTIONSTEST_2 <=== 25+(2^-3^-(4+5*2) *100)
push Const 25
push Const 2
push Const 3
neg
push Const 4
push Const 5
push Const 2
mul
add
neg
exp
exp
push Const 100
mul
add
Die von mir vorgeschlagene
richtige Prozedur Exponentiation():
Code: Alles auswählen
Procedure Exponentiation() ; *** richtig: rechts_assoziativ ***
; --> Token/Lexem steht auf Value
; Aufstieg zu NegValueFactor()
NegValueFactor()
; Power-Op in Token/Lexem? <==== IF statt WHILE
If Scanner::Token='^'
; vor Aufstieg nächstes Token (=Value) holen
Scanner::GetToken()
; Rechtsassoziativ !!! --> Selbstaufruf
; ====> BEI SELBSTAUFRUF RICHTIG RECHTSASSOZIATIV !!! <==== ÄNDERUNG HIER
Exponentiation()
; Ausgabe des ASM-Codes des Operators
Code::_Exp()
EndIf
; --> in Token/Lexem ist Token/Lexem nach dem Operator ODER
; --> in Token/Lexem ist Token/Lexem nach dem Value
; --> Abstieg zu MulTerm()
EndProcedure
Erklärung:
- Mein Lösungsansatz besteht darin, dass sich die richtige Prozedur Exponentation() immer wieder selbst aufruft - bis zum Ende der Kette der "^"-Operatoren (Beispiel: 2^3^4 +5).
- Am Ende der "^"-Operatorenkette (also bei '+' im Beispiel) beginnt die Prozedurenaufrufkette wieder rückwärts zu laufen und arbeitet alle "exp"-Befehle bis zum letzten ab.
- Dadurch wird meiner Meinung nach der Teilausdruck, der aus "^"-Operatoren besteht, rechtsassoziativ.
- Bitte nicht übersehen, dass ich nicht nur den zweiten Aufruf zur nächsthöheren Prozedur zu einem Selbstaufruf verändert habe, sondern auch die While-Schleife zu einer If-Then-Konstruktion.
- Wie oben bereits von mir erwähnt, bitte ausgiebig testen.
Ich lasse beide Prozedur-Versionen zum Testen beider Varianten, einmal der richtigen unter dem Prozedurnamen Exponentation() und einmal der falschen unter dem Namen Exponentation_LA() ('_LA' steht für linksassoziativ), im Code-Teil von Kapitel 9.
9.4. Hinzufügen des unären logischen Not
9.4.1. Vorüberlegungen
Die Sache mit dem logischen Not-Operator (nicht zu verwechseln mit dem bitweisen Not-Operator) ist auf den ersten Blick einfach, auf den zweiten schwierig.
Nicht dass seine Umsetzung besonders schwierig wäre, das Wie ist also nicht das Problem an sich, aber die Qual der
Entscheidung,
wo wir den unären Not-Operator
in die Präzedenztabelle einfügen, ist groß.
Verschiedene Sprachen gehen da
verschiedene Wege:
- In Pure Basic finden wir "not" recht weit unten, ebenso in Python.
- C und Pascal hingegen haben "not" sehr weit oben in der Präzedenztabelle.
Ich würde für mich persönlich (siehe Code-Teil des Kapitels 9) eher die Variante von
Pure Basic bevorzugen und das hat seinen Grund (siehe Diskussion nächstes Unterkapitel).
9.4.2. Diskussion: logisches Not und die Operatoren and, or und xor?
Ich folge hier einem Argument von Crenshaw im Kapitel
http://www.pp4s.co.uk/main/tu-trans-comp-jc-16.html.
Crenshaw als Naturwissenschaftler weiß, dass man den booleschen Operator "xor" (exlusives Or) auch mit anderen booleschen Operatoren umschreiben kann.
Folgende Schreibweise für die
Umschreibung des Xor sei
üblich in der Naturwissenschaft, meint er:
Wenn wir jetzt erlauben würden, dass "and" eine höhere Präzedenz als "not" hat, dann würde dieser Ausdruck folgendermaßen gelöst:
Code: Alles auswählen
a xor b >= (a and not b) or ( not (a and b) )
^^^ ^^^
Daraus schließt Crenshaw, dass der
Operator "not" eine höhere Präzedenz als "and" und "or/xor" haben muss, eine Bedingung, die in allen mir bekannten Sprachen auch erfüllt ist. Wäre das nicht so, dann kann die obere Umschreibung von "xor" nicht optisch so ausschauen, wir müssten eben die Klammer bei "( not (a and b) )" einfügen und das sei aber, laut Crenshaw, nicht das Aussehen, das ein Wissenschaftler von diesem Ausdruck erwarten würde. Also braucht es eine höhere Präzedenz für "not".
Ich schließe mich ihm an, dass
"not" auf jeden Fall höhere Priorität als "and" und "or/xor" haben muss, das ist in den meisten Sprachen der Fall, auch in Pure Basic.
Wenn wir uns an die
Präzedenztabelle von
Pure Basic erinnern:
Code: Alles auswählen
-----------------+---------------------
Prioritäts-Level | Operatoren
-----------------+---------------------
8 (hoch) | ~, -
7 | <<, >>, %, !
6 | |, &
5 | *, /
4 | +, -
3 | >, >=, <, <=, =, <>
2 | Not
1 (niedrig) | And, Or, XOr
-----------------+---------------------
dann sehen wir aber noch etwas.
"Not" ist hier nämlich von sehr niedriger Präzedenz, wohl richtigerweise noch höher als "and" und "or/xor", aber sonst ganz unten in der Tabelle.
9.4.3. Diskussion: logisches Not und die restlichen Operatoren?
Wie hoch soll die Präzedenz des Not-Operators in Bezug auf die übriggebliebenen Operatoren sein?
Wie es um "and, or, xor" steht, wissen wir ja bereits, denn wir haben uns mit Crenshaw (und den anderen Programmiersprachen) darauf geeinigt, dass "not" auf jeden Fall eine höhere Präzedenz als "and" und "or/xor" haben soll, um die Umschreibung für "xor" korrekt aussehen zu lassen. Damit hätten wir die naturwissenschaftliche bzw. ingenieurswissenschaftliche Mathematik befriedigt bzw. nicht gegen uns aufgebracht.
Aber was ist mit all den anderen?
9.4.3.1. Not-Operator mit sehr hoher Präzedenz (ohne Code)
Präzedenztabelle für
not mit sehr hoher Präzedenz:
Code: Alles auswählen
Prozedur Operator Assoziativität Präzedenz
-----------------------------------------------------------------------
ValueFactor() ( ... ) höchste
-----------------------------------------------------------------------
NegValueFactor() Negation L *-->
bzw. Unäres -|+
-----------------------------------------------------------------------
LogNot() unäres log. not R <--*
-----------------------------------------------------------------------
Exponentiation() ^ R <--*
-----------------------------------------------------------------------
MulTerm() * / % L *-->
-----------------------------------------------------------------------
AddExpression() + - L *-->
----------------------------------------------------------------------
Relation() < <= <> = >= > L *-->
-----------------------------------------------------------------------
BoolAnd() and L *-->
-----------------------------------------------------------------------
BoolOrXor() or xor L *--> niedrigste
-----------------------------------------------------------------------
Eingefügt habe ich "
not" schnell mal zum Testen zwischen Exponentiation() und NegValueFactor() (ich denke, das bekommt mittlerweile jeder selbst auch hin).
Ich folge hier ganz der bereits erwähnten Präzedenztabelle von C (
http://montcs.bloomu.edu/~bobmon/Inform ... scal.shtml), aber auch in Pascal finden wir "not" ganz weit oben (siehe ebenfalls voriger Link).
Folgen wir diesem Gedanken kurz einmal und überlegen wir uns, wo uns das beim Rechnen hinbringt.
Rechnen wir folgenden
Source-Code mit der obigen Präzendenztabelle durch:
Code: Alles auswählen
not a + 5 * 3 // sollte unserer derzeitigen Präzedenztabelle
// gemäß so gelöst werden: (not a) + 5*3
VM-ASM im
ta.-File zeigt, dass unsere
Annahme richtig war:
Es geht also "
not", was ja so sein muss, wenn wir uns die Präzedenztabelle betrachten,
direkt auf den ValueFactor, die restliche Rechnung kommt danach.
Wenn ich hier den
ganzen Ausruck "a+5*3" mit "not" versehen will, brauche ich
Klammern:
In C müsste ich mit Klammern arbeiten, in Pascal ebenfalls, aber eben
nicht in Pure Basic zum Beispiel, weil da alle anderen Operatoren (außer "and, or, xor") höherwertig sind als "not".
Ein letztes
Source-Code-Beispiel zu diesem Diskussionspunkt:
Der Compiler übersetzt in folgendes
.ta-File:
Man sieht auch hier wieder sehr schön, dass "
not" hoher Präzedenz immer
zuerst auf den (ev. negativen) ValueFactor geht und nicht auf einen größeren (Teil-)ausdruck.
Wenn ich das so haben will, dann werde ich meinen mathematischen Parser genau so programmieren. Die Prozedur
LogNot() füge ich genau dort ein.
Ich kann die Prozedur aus dem Code-Teil von Kapitel 9 hernehmen und wie immer nur die
Aufrufe davor und in der Prozedur
entsprechend anpassen und schon habe ich "not" auf dieser Ebene.
Im Code-Teil ist die Prozedur LogNot() allerdings nicht so weit oben, sondern gleich über "and, or, xor" zu finden, also wie in Pure Basic, weil diesen Compiler ich schreibe und mir diese Präzedenz besser gefällt

.
Und letztlich ist es die Entscheidung des Compiler-Schreibers, wo er sein Not haben will. Das muss jeder für sich selbst entscheiden, denn feste Regeln gibt es hier nicht.
9.4.3.2. Not-Operator mit Präzedenz genau über "and, or, xor" (mit Code)
Ich möchte jetzt hier wieder einmal zur Übersicht den Declare-Teil der Prozeduren des mathematischen Parsers sowie die Präzedenztabelle vorstellen.
Declare-Teil unseres mathematischen Parsers mit einem
Not niedrigerer Präzedenz:
Code: Alles auswählen
; --- Mathematischer Parser ---
Declare ValueFactor() ; holt einen Operanden-Wert, ( ... )
Declare NegValueFactor() ; unary -|+
Declare Exponentiation() ; Potenz: Hoch-Operator: ^ (*rechts assoziativ*)
Declare MulTerm() ; bearbeitet Mul-Ops: *, /, %
Declare AddExpression() ; bearbeitet Add-Ops: +, -
Declare Relation() ; logische Vergleiche <, <=, <>, =, >=, >
Declare LogNot() ; unary logisches Not
Declare BoolAnd() ; boolesches And
Declare BoolOrXor() ; boolesches Or, Xor
Declare Expression() ; mathematischer Expression Parser 3+4*(4+3)
Präzedenztabelle für
not mit niedrigerer Präzedenz:
Code: Alles auswählen
Prozedur Operator Assoziativität Präzedenz
-----------------------------------------------------------------------
ValueFactor() ( ... ) höchste
-----------------------------------------------------------------------
NegValueFactor() Negation L *-->
bzw. Unäres -|+
-----------------------------------------------------------------------
Exponentiation() ^ R <--*
-----------------------------------------------------------------------
MulTerm() * / % L *-->
-----------------------------------------------------------------------
AddExpression() + - L *-->
----------------------------------------------------------------------
Relation() < <= <> = >= > L *-->
-----------------------------------------------------------------------
LogNot() unäres log. not R <--*
-----------------------------------------------------------------------
BoolAnd() and L *-->
-----------------------------------------------------------------------
BoolOrXor() or xor L *--> niedrigste
-----------------------------------------------------------------------
Mit dieser Präzedenzeinstellung geht "
not"
auf die gesamte Expression und nicht nur auf einen einfachen (ev. negativen) ValueFactor. Der Not-Operator lässt also
einen mathematischen Ausdruck VORHER (außer natürlich wie aus der Tabelle ersichtlich "and" und "or/xor".) berechnen und nimmt
dessen Ergebnis erst als "not".
Das ist das Verhalten von Pure Basic und auch Python, nicht aber von C oder Pascal, man sieht: Wirklich bindende Regeln gibt es hier nicht.
Am besten ich bringe
ein Beispiel:
Code: Alles auswählen
not a+32*4 // dieser Code müsste folgender
not (a+32*4) // Reihenfolge entsprechen, also Teil-Expression zuerst
Im
.ta-File finden wir (Kommentare und Leerzeichen wie immer von mir):
Code: Alles auswählen
push Global A <=== not a+32*4, Reihenfolge ist not (a+32*4)
push Const 32
push Const 4
mul
add
not
push Global A <=== not (a+32*4)
push Const 32 <=== MIT OBEN IDENT, WEIL COMPILER RECHNET
push Const 4 <=== DURCH PRÄZEDENZ AUTOMATISCH GENAU SO
mul
add
not
Beide Codes sind ident, mit und ohne Klammer, d.h. der Compiler berechnet zuerst den mathematischen Teil-Ausdruck "a+32*4", dessen Operanden mit Operatoren höherer Präzedenz als "not" verbunden sind.
Jetzt können wir in
Toy-C intuitiv Folgendes tun:
Code: Alles auswählen
if (not 5+3*4) // das geht wie in Pure Basic
if (not(5+3*4)) // Klammer dazu nicht notwendig
Zum Verständis hier zum
Vergleich denselben Code kompiliert mit dem Parser (voriges Unterkapitel), der ein "
not"
ganz hoher Präzedenz hat:
Code: Alles auswählen
push Global A <=== not a+32*4, also Reihenfolge ist (not a)+32*4
not <=== NOT geht sofort auf den Value
push Const 32
push Const 4
mul
add
push Global A <=== not (a+32*4)
push Const 32 <=== diese Reihenfolge müssen wir mit einer Klammer erzwingen
push Const 4
mul
add
not
Die Operatoren "
and, or, xor" haben eine
höhere Präzedenz als "not". Also testen wir folgenden
Source-Code:
Code: Alles auswählen
(a and not b) or ( not a and b )
XXXXXXXXXXXXXXXXXXXXXX
(a and not b) or ( (not a) and b ) // so (lt. Crenshaw) will es die Naturwissenschaft verstehen
Und unser Compiler schreibt genau das ins
.ta-File:
Code: Alles auswählen
push Global A
push Global B <=== BEIDES IDENT, RECHNET ALSO,
not <=== ALS WÄRE DIESE KLAMMER DA
and
push Global A
not
push Global B
and
or
push Global XXXXXXXXXXXXXXXXXXXXXX
push Global A
push Global B <=== BEIDES IDENT, RECHNET ALSO,
not <=== ALS WÄRE DIESE KLAMMER DA
and
push Global A
not
push Global B
and
or
Heureka!
Hier die fertige
Prozedur LogNot() zum Abschluss:
Code: Alles auswählen
Procedure LogNot()
; --> Token/Lexem steht auf Value ODER
; --> auf unary not
; Ist ein 'unary not' vor ValueFactor? <==== IF statt WHILE
If Scanner::Lexem = "NOT"
; vor Aufstieg nächstes Token holen
; d.i 'not' ODER ein Value
Scanner::GetToken()
; Selbstaufruf für 'not not not ...' <==== VGL. MIT EXPONENTATION()
LogNot()
; Ausgabe des ASM-Codes des Operators
Code::_LogNot()
Else
; Aufstieg zu Relation()
Relation()
EndIf
; --> in Token/Lexem ist Token/Lexem nach Value
; --> wenn die Expression weitergeht, ist das ein Operator
; --> Abstieg zu BoolAnd()
EndProcedure
Die Prozedur LogNot() hat
gewisse Ähnlichkeit mit Exponentation(). Auch hier haben wir einen Selbstaufruf im Code, um
mehrere Not-Operatoren hintereinander zu erlauben.
9.4.3.3. Eine noch niedrigere Präzedenz für not?
Was spricht eigentlich gegen eine noch niedrigere Präzedenz?
Außer der oben erwähnten Naturwissenschaft, die ein gewaltiges Gegenargument hat, nichts.
Ich für meinen Teil möchte über niemanden urteilen, der sein "not" noch tiefer in der Präzedenztabelle gibt - das kann jetzt aber wirklich schon jeder, einfach LogNot() über Expression() und vor BoolOrXor() verschieben und Prozeduraufrufe anpassen -, denn das hätte auch durchaus seine Vorteile.
Vergleichen wir hier beide Interpretationsmöglichkeiten,
je nach Präzedenz:
Code: Alles auswählen
not a<2 and b>4 /* bei der derzeitigen Einstellung rechnet der Compiler:
/* (not a<2) and (b>4)
/* ^^^-- hier endet "not", weil "and" niedriger
not (a<2 and b>4) /* wir hätten vielleicht gewollt, dass "not"
/* auf die gesamte Expression geht
/*
Hier beide Möglichkeiten als VM-ASM-Code:
Code: Alles auswählen
push Global A <=== (not a<2) and (b>4)
push Const 2
lt
not
push Global B
push Const 4
gt
and
push Global A <=== not (a<2 and b>4)
push Const 2
lt
push Global B
push Const 4
gt
and
not <--- ganz am Ende erst "not", also Gesamtexpression
Der Nachteil dieser Präzedenzstufe ist, wir müssen bei der "xor"-Umschreibung auf unmathematische Weise die Klammern um "( (not a) and b )" setzen.
Das soll jeder für sich entscheiden!
9.4.3.4. Einige Worte zur Assoziativität von not
Manche Programmiersprachen bezeichnen den Not-Operator als links- und manche als rechtsassoziativ. Da es sich hier aber um einen unären Operator handelt, stehe ich dazu, dass mich die Sache etwas verwirrt.
Ich als Nichtmathematiker habe mich gefühlsmäßig dem Lager der "Rechtsassoziativen" angeschlossen, weil auch die Compilerausgabe danach aussieht, dass am Ende einer Not-Kette von rechts nach links über die Rücksprünge der vorher aufgerufenen Prozeduren zurückgekehrt wird.
Im Endeffekt ist aber diese Festlegung, nach allem was ich gesehen habe, nicht wirklich wichtig und hat lediglich Einfluss auf den Tabelleneintrag von "not" in der Präzedenztabelle.
Sollte sich im Forum jemand mit tiefergehenden mathematischen Kenntnissen befinden, um uns den Unterschied auseinanderzusetzen, dann bitte ich darum, sich zu outen!
Ein kleines Beispiel in
Source-Code und dann lasse ich die Sache auf sich beruhen, weil sie - glaube ich zumindest - eigentlich keinen Einfluss hat:
Code: Alles auswählen
not not not not not -A + B /* eigentlich irgendwie völlig sinnlos
/* zeigt Rücklauf der Prozeduren
*/
Im
.ta-File finden wir:
Code: Alles auswählen
push Global A
neg
push Global B
add
not
not
not
not
not
Perfekt!!
Nicht vergessen dürfen wir, dass im vorigen Unterkapitel die Präzedenz von "not" von mir recht weit (auf Pure-Basic-Niveau) heruntergesetzt wurde, der Compiler übersetzt richtigerweise die (Teil-)expression "(-A + B)" vorher, dann läuft er die Not-Operatoren rückwärts.
Erneut können wir in
http://montcs.bloomu.edu/~bobmon/Inform ... scal.shtml die Assoziativität von "not" nachlesen, hier sind die meisten sogar für linksassoziativ

.
Meine obige Bitte an die Mathematiker hier im Forum ist weiterhin aufrecht

.
9.5. Kurz ein wenig Error Handling (Fehlerbehandlung)
Was passiert eigentlich, wenn wir bei einer
unären Operation versehentlich zu viele z.B. "-" schreiben?
- Probieren wir das einmal in Pure Basic und in Toy-C:
Wir erhalten in Pure Basic eine korrekte Fehlermeldung (Syntax Error)!
Und in Toy-C?
Wir bekommen ebenfalls eine Fehlermeldung, unser Compiler erkennt sogar, dass ein "Operand" fehlt, denn beim 1. Minus kennt er sich noch aus (sub), beim 2. Minus erwartet er "unary -" (neg), beim 3. Minus hätte er gerne wieder einen Operanden (Value: also Zahl oder Variablen- oder Prozedurname).
Was passiert eigentlich, wenn wir bei einem Operator, der aus einem
Wort besteht (z.B. and, or, not, ...) und deshalb genauso gut ein Variablenname sein könnte, einen Fehler machen?
- Da in Pure Basic mixed Operations - in der Zusammenfassung am Ende von Kapitel 9 Genaueres darüber - nicht erlaubt sind, wähle ich für PB folgendes Beispiel:
Code: Alles auswählen
If X<2 And Or X>=3 : EndIf
^^^^--- PB denkt, dies wäre ein Variablenname
PB gibt die richtige Fehlermeldung aus, meint doch der Compiler korrekterweise, dass jetzt ein Operator kommen müsste, erkennt ein "Or" und findet heraus, dass diese "Variable" ein Pure-Basic-Schlüsselwort ist. Fehler!
In Toy-C können wir das direkt in Source-Code eingeben:
Unser Toy-C-Compiler glaubt in dieser Situation hingegen einfach, dass z.B. ein "or" ein Variablenname ist:
Code: Alles auswählen
push Global X
push Const 2
lt
push Global OR <=== ValueFactor soll kommen, na dann: OR = Variable! FALSCH!
and
push Global X
push Const 3
ge
Das können wir so lassen und damit leben oder auch nicht.
Ich habe einen
Workaround (Englisch: um etwas herum arbeiten, also eine schnelle Lösung, die das Problem zunächst einmal löst)
in ValueFactor() eingebaut.
9.6. Zusammenfassung
Wir haben nun einen
mathematischen Parser, der folgende
Spezifikationen aufweist:
- Klammerausdrücke ( ... )
- Unary Operationen: + - not
- Korrekte Präzedenzbehandlung unserer Wahl (siehe Präzedenztabelle) aller Operatoren
- korrekte Rechstassoziativität des Potenz-Operators ^
- mathematische Ausdrück erlauben gemischte Operationen (mixed Operations) aller Art
- einfachste Fehlererkennung
Was auf den ersten Blick noch fehlt, sind einige Operatoren, die ich aber derzeit nicht brauche, z.B. die bitweisen Operatoren (z.B. | ~ &) oder die Bitschieboperatoren (z.B. << >>).
Vielleicht baue ich sie ein, vielleicht können das die Leser aber auch schon selbst mittlerweile, denn das ist flott erledigt.
Interessant ist, dass
gemischte z.B. boolesche und nicht boolesche Operatoren, also das gleichzeitige Auftauchen von z.B. "and" oder "or" und auch z.B. "*" oder "-" uvm. in einem mathematischen Ausdruck
problemlos möglich ist.
Pure Basic verbietet das, und ich glaube, dass man das sogar absichtlich mit Mehraufwand verbieten muss, denn an sich, wie man an unserem Parser sieht, ist das von Haus aus erlaubt:
Ein Beispiel:
Wenn wir folgendes
Programm in Pure Basic starten, erhalten wir eine Fehlermeldung (zum Verständnis unbedingt starten und durchlesen):
Code: Alles auswählen
Debug 5+(3<2) ; Fehler! Das geht in PB nur innerhalb von If, While, Until und Bool()
;
; * Das müsste man in PB so umschreiben
;
Debug 5+Bool(2<3) ; ergibt 6, weil 2<3=#True (also 1)
Debug 5+Bool(3<2) ; ergibt 5, weil 3<2=#False (also 0)
;
; * Toy-C braucht Bool() nicht, da geht das einfach so
;
Ja, das ist halt sowas von Basic

.
Toy-C akzeptiert das wie seine größeren Brüder problemlos:
wird zu einem [/b].ta-File[/b] mit folgendem Inhalt:
Die abschließende
Präzedenztabelle des Compilers von
Kapitel 9:
Code: Alles auswählen
=======================================================================
Prozedur Operator Assoziativität Präzedenz
=======================================================================
ValueFactor() ( ... ) höchste
-----------------------------------------------------------------------
NegValueFactor() Negation L *-->
bzw. Unäres -|+
-----------------------------------------------------------------------
Exponentiation() ^ R <--*
-----------------------------------------------------------------------
MulTerm() * / % L *-->
-----------------------------------------------------------------------
AddExpression() + - L *-->
----------------------------------------------------------------------
Relation() < <= <> = >= > L *-->
-----------------------------------------------------------------------
LogNot() Unäres log. not R <--*
-----------------------------------------------------------------------
BoolAnd() and L *-->
-----------------------------------------------------------------------
BoolOrXor() or xor L *--> niedrigste
=======================================================================
Die EBNF kann sich bei zu viel Zeit und Laune jeder selbst erarbeiten, ergibt sich ohnehin aus den Prozeduren und der Präzedenztabelle.
9.7. Code des Compilers von Kapitel 9
- ---- weiter im Teil "Code" dieses Kapitels ----
Viel Spaß mit dem Compiler von Kapitel 9.
LG Puretom
*** ENDE KAPITEL 9: TEXT ***
DAS KANN MAN SCHON ERNSTER NEHMEN: BITTE UM BUGBERICHTE, FEEDBACK, LOB, ..., denn das war viel Arbeit