Scriptsprache
Sie verwenden einen veralteten Browser (%browser%) mit Sicherheitsschwachstellen und können nicht alle Funktionen dieser Webseite nutzen.
Hier erfahren Sie, wie einfach Sie Ihren Browser aktualisieren können.
Hier erfahren Sie, wie einfach Sie Ihren Browser aktualisieren können.
Es gibt 41 Antworten in diesem Thema. Der letzte Beitrag () ist von nikeee13.
-
-
-
Hi
was wäre der große Architekturunterschied zwischen
push LinePointer
;push args
goto 'MyFunction'
'MyFunction':
;body
;pop args
goto pop
und
Function MyFunction(firstArg, secondArg)
Return firstArg + secondArg
End Function
bzw. vllt. echt funktional
MyFunction a b = a + b
? Du erhöhst die Komplexität des Programms damit nicht besonders und es macht Spaß, das so zu entwickeln, besonders, weil man eben den Umweg über einen Stack und ein Array gehen kann. Im Array stehen lauter Delegaten oder entsprechende Instanzen von primitiven Opcodes, die nach-und-nach auf einen Stapel angewendet werden können. Der Stapel hat dabei Operationen, die z.B. zwei Variablen herunternehmen können (von links (2. Pop) nach rechts (1. Pop)).
Meine erste Skriptsprache hat so gearbeitet (waren nur erste Gehversuche, daher auch ohne Tokenizer und allem - ja selbst ohne Architektur!!).
Gruß
~blaze~ -
@~blaze~
Klar, so geht es auch. Jedoch geht es mir eher darum, solche Sachen wie Tokenizer und so weiter zu verstehen und, wie ich jetzt schon mehrmals gesagt habe, eine möglichst simple Skriptsprache zu erstellen, bei der es ausreicht, auch nur minimale bis gar keine Kenntnisse in dem Bereich braucht. -
Ich denke, dass es auch so für dich interessant sein könnte. Am Tokenizer musst du ja nichts machen, der ist eh total einfach zu programmieren. Syntaktische Analyse selbst muss man sich halt bisschen was überlegen, aber bei einer ansprechenden Architektur ist das auch nicht so schwer. Man muss halt nur das Pattern zuordnen, somit sind das eigentlich einfach nur mehr Pattern. Der Witz dahinter steckt eigentlich eher darin, dass du in so einem Fall auch noch lernst, wie du die zugänglichen Member referenzierst. Z.B. gilt ja eine Deklaration innerhalb einer Funktion nicht außerhalb. Die Funktionalität erhöhst du dabei außerdem noch stark, da du Rekursion ohne hässliche Patterns, wie goto einfach umsetzen kannst.
Ich will mich da jetzt nicht weiter einmischen, aber es wäre halt praktisch und auch vom Lernaufwand her sicher nicht schlecht, wenn du sowas noch nicht gemacht hast. Viel mehr Arbeit wäre es auch nicht.
Um noch was konstruktives zu liefern: Ich würde If, While/Until bzw. Do-Loop als gemeinsames ConditionStatement mit einer Looped-Eigenschaft modellieren, das die Condition verzögert abfragen kann, sofern es eine gibt für Do-Loop-<condition>.
Gruß
~blaze~ -
Danke für den Tipp, jedoch glaube ich nicht, dass ich das in diesem Projekt umsetzen werde, einfach weil es in meinen Augen für ganz einfache Programme einfach keinen Sinn macht. Jedenfalls glaube ich kaum, dass irgendjemand dieses Feature benutzen würde, außerdem kann man es dann sicher auch im Nachhinein ausbauen.
-
Wenn du willst, kann ich bei Gelegenheit mal einen Blick auf die Architektur werfen, wenn du nicht sicher bist, ob sie dafür tauglich ist. Bei meinem derzeitigen Projekt habe ich auch die Patterns abstrakt modelliert (und dabei brav overengineert). Dadurch ist da die syntaktische Analyse sehr einfach durch das Hinzufügen von Pattern zur Hierarchie möglich.
Gruß
~blaze~ -
Du könntest mal nen Blick auf bestehende Projekte werfen und dir dort Ideen holen.
Hier mal ein guter, aber einfacher, Tokenizer, welcher auch leicht zu verändern ist: codeproject.com/Articles/7664/StringTokenizer
Und hier ein paar OS Projekte:
The Microsoft NET version of the paxScript scripting engine (paxScript.NET) includes interpreters of C# and VB.NET languages.
eco148-88394.innterhost.net/paxscriptnet/
Dynamic Expresso is an expression interpreter for simple C# statements. Dynamic Expresso embeds its own parsing logic, and really interprets C# statements by converting it to .NET delegates that can be invoked as any standard delegate. It doesn't generate assembly but it creates dynamic expressions/delegates on the fly.
github.com/davideicardi/DynamicExpresso/
CS-Script is a CLR (Common Language Runtime) based scripting system which uses ECMA-compliant C# as a programming language. CS-Script currently targets Microsoft implementation of CLR (.NET 2.0/3.0/3.5/4.0/4.5) with full support on Mono.
csscript.net/index.html
Jedes dieser Projekte verwendet entweder C# oder (wie paxScript) VB.NET als ScriptSprache. Von denen könntest du dir abschauen, wie die das Parsen der Texte lösen.SWYgeW91IGNhbiByZWFkIHRoaXMsIHlvdSdyZSBhIGdlZWsgOkQ=
Weil einfach, einfach zu einfach ist! -
Ich habe mich mal daran versucht, einen kleinen Tokenizer zu erstellen:
Spoiler anzeigen VB.NET-Quellcode
- Public Class Tokenizer
- Private LineSeperators As String() = {ControlChars.Cr, ControlChars.Lf}
- Private TokenSeperators As Char() = {" "c}
- Private StringIndicator As Char = """"c
- Private CommentIndicator As Char = "'"c
- Public Function Tokenize(code As String) As List(Of Token())
- Debug.WriteLine("----------------------------------------------------------------------")
- 'Den Code in die einzelnen Zeilen zerteilen
- Dim Lines As IEnumerable(Of String) = code.Split(LineSeperators, StringSplitOptions.RemoveEmptyEntries)
- Dim LineEnumerator As IEnumerator = Lines.GetEnumerator
- 'Alle Zeilen durchgehen
- Dim TokenList As New List(Of Token())
- For LineIndex As Integer = 0 To Lines.Count - 1
- Dim Line As String = Lines(LineIndex)
- Dim LineNumber = LineIndex + 1
- Debug.WriteLine(String.Format("Analyzing Line {0}: '{1}'", LineNumber, Line))
- 'Hier kommen die Tokens für diese Zeile rein
- Dim Tokens As New List(Of Token)
- Dim IsString As Boolean = False
- Dim IsComment As Boolean = False
- Dim Buffer As New List(Of Char)
- For Index As Integer = 0 To Line.Length - 1
- 'Der aktuelle Char
- Dim CharAtIndex As Char = Line(Index)
- Select Case True
- Case IsComment
- 'Nichts machen
- Buffer.Add(CharAtIndex)
- Case CharAtIndex = StringIndicator 'String fängt an oder hört auf
- If IsString Then
- 'Zeichen in den Buffer hinzufügen
- Buffer.Add(CharAtIndex)
- 'Buffer in Token speichern und abspeichern
- Tokens.Add(New Token(String.Join("", Buffer.Select(Function(c) c.ToString).ToArray)))
- 'Buffer leeren
- Buffer.Clear()
- Else
- 'Nichts machen
- Buffer.Add(CharAtIndex)
- IsString = True
- End If
- Case CharAtIndex = CommentIndicator 'Kommentar fängt an (außer er ist in einem String)
- 'Nur was machen, wenn sich das Kommentarzeichen nicht
- If Not IsString Then
- 'Speichern, dass ein Kommentar aktiv ist
- IsComment = True
- 'Zeichen dem Buffer hinzufügen
- Buffer.Add(CharAtIndex)
- Else
- 'Nichts machen
- Buffer.Add(CharAtIndex)
- End If
- Case TokenSeperators.Any(Function(c) c = CharAtIndex) 'Es handelt sich um einen Tokentrenner
- If (Not IsString) AndAlso (Not IsComment) Then
- 'Buffer in Token speichern und abspeichern
- If Buffer.Count > 0 Then
- Tokens.Add(New Token(String.Join("", Buffer.Select(Function(c) c.ToString).ToArray)))
- 'Buffer leeren
- Buffer.Clear()
- End If
- Else
- 'Nichts machen
- Buffer.Add(CharAtIndex)
- End If
- Case CharAtIndex = "("c 'Klammer auf
- If Buffer.Count > 0 Then
- 'Zeichen dem Buffer hinzufügen
- Buffer.Add(CharAtIndex)
- 'Buffer in Token speichern und abspeichern
- Tokens.Add(New Token(String.Join("", Buffer.Select(Function(c) c.ToString).ToArray)))
- 'Buffer leeren
- Buffer.Clear()
- Else
- 'Klammer in eigenem Token abspeichern
- Tokens.Add(New Token(CharAtIndex.ToString))
- End If
- Case CharAtIndex = ")"c 'Klammer zu
- 'Buffer in Token speichern und abspeichern
- If Buffer.Count > 0 Then
- Tokens.Add(New Token(String.Join("", Buffer.Select(Function(c) c.ToString).ToArray)))
- 'Buffer leeren
- Buffer.Clear()
- End If
- 'Klammer in Token abspeichern
- Tokens.Add(New Token(CharAtIndex.ToString))
- Case Else
- 'nichts spezielles machen
- Buffer.Add(CharAtIndex)
- End Select
- Next
- 'Letzten Token abschließen
- If Buffer.Count > 0 Then
- Tokens.Add(New Token(String.Join("", Buffer.Select(Function(c) c.ToString).ToArray)))
- 'Buffer leeren
- Buffer.Clear()
- End If
- Debug.WriteLine(String.Format(" Splitted Line in Tokens: '{0}'", String.Join("' '", Tokens.Select(Function(t) t.ToString).ToArray)))
- 'Tokens abspeichern
- TokenList.Add(Tokens.ToArray)
- Next
- 'Alle Tokens zurückgeben
- Return TokenList
- End Function
- End Class
Hier mal ein kleiner Beispielcode:
und der dazugehörige Output des Tokenizers (nur das aus den Debugmeldungen, das Verarbeiten kommt noch ;):
Quellcode
- Analyzing Line 1: 'X <- (sin(cos(19)) + 8) / 10 'Variable zuweisen'
- Splitted Line in Tokens: 'X' '<-' '(' 'sin(' 'cos(' '19' ')' ')' '+' '8' ')' '/' '10' ''Variable zuweisen'
- Analyzing Line 2: 'Y <- Input("Hallo Welt, bitte geben sie eine Zahl ein:")'
- Splitted Line in Tokens: 'Y' '<-' 'Input(' '"Hallo Welt, bitte geben sie eine Zahl ein:"' ')'
- Analyzing Line 3: ''Jetzt das noch ausgeben'
- Splitted Line in Tokens: ''Jetzt das noch ausgeben'
- Analyzing Line 4: 'Output(Y)'
- Splitted Line in Tokens: 'Output(' 'Y' ')'
Was denkt ihr? Hab ich da jetzt irgendwas komplett falsch gemacht oder habt ihr noch Verbesserungsvorschläge?
PS: Der Code ist noch etwas unstrukturiert und alles, sollte aber einigermaßen verständlich sein. -
Ein Tokenizer wird normalerweise mit einer StateMachine implementiert. Du gehst schon in die richtige Richtung, indem du die Zeichen einzeln verarbeitest. Wenn deine Sprache aber komplizierter wird, bekommst du bei deiner Variante Probleme mit zu vielen IsWhatever-Variablen, weil du dir beim Speichern des Zustands nicht die Objektorientierung zunutze machst.
Erstell dir eine Basisklasse "Tokenizer" und leite davon alle Konstrukte ab, die du erkennen willst. Ein Klammerausdruck besteht z.B. aus der Subklasse RoundParen, die von "( EXPRESSION )" die öffnende und schließende Klammer erkennt. EXPRESSION selbst wird von einer weiteren Subklasse erkannt. Jede Subklasse überschreibt dabei immer dieselbe Methode "Overridable Sub ParseNext(currentChar As Char, nextChar As Char)" oder ähnlich. Das Token kommt dann in einem globalen Event in der Basisklasse, z.B. wenn RoundParen eine öffnende oder schließende Klammer erkannt hat.
Bei der Analyse brauchst du nicht nur das aktuelle Zeichen, sondern auch das Nachfolgende. Das nennt sich LL(1)-Parser. Schau dir mal ein paar Uni-Vorlesungen dazu an - such nach theoretischer Informatik oder nach Compilerbau.
Und noch was anderes, was in deiner Ausgabe seltsam erscheint: Du solltest die öffnende Klammer vom Namen der Funktion trennen. Ansonsten scheiterst du an einem Ausdruck wie 5 - (1 + 2).Gruß
hal2000
-
@hal2000
Wieso sollte ich an so einem Ausdruck scheitern? Das Minus davor wird mit meiner momentanen Methode automatisch in einen eigenen Töten gesetzt, da nach dem Minus ein Leerzeichen ist, welches als Trennen dient.
Im übrigen steht das Gerüst dahinter schon, ich habe bisher einfach noch nicht den Tokenizer mit dem Rest verbunden, da ich mir nicht sicher war, ob ich das mit dem Tokenizer richtig mache.
PS: Momentan dachte ich eher an ein 3-Schritte-System (Tokens erstellen, Tokens zu einem Typ zuordnen, Interpretieren). Was wäre jetzt der Vorteil von deiner Methode? -
Mir ist gerade aufgefallen, dass ja auch einzelne öffnende Klammern erkannt werden. Dann ist das in Ordnung. Allerdings muss dein Tokenizer auch funktionieren, wenn kein Leerzeichen zwischen Minus und Klammer steht. Kommt dann als Token '-(' raus? Das wäre nämlich ungünstig. Die StateMachine würde das Minus als Token ausgeben und bei der Klammer in einen neuen Zustand gehen.
Das 3-Schritte-System ist so korrekt - die Tokens hast du ja schon. Als nächstes werden die Typen abstrahiert, also z.B. '12' zu NUMBER, '+', '-', ... zu OPERATOR usw. Vor der Interpretation solltest du die Tokens auf syntaktische Korrektheit prüfen. Hast du dich dafür schon mit Grammatiken beschäftigt? Wenn nicht, kannst du auch erstmal drauflosinterpretieren und bei Fehlern einfach abbrechen (was ein Interpreter ja auch normalerweise tut).Gruß
hal2000
-
Wie gesagt, es handelt sich bei dem Teil bisher nur um einen Prototypen, außerdem möchte ich bei der Syntax möglichst wenig tolerieren. Was meinst du mit Grammatiken (ich habe mir erstmal absichtlich gar nichts angeschaut, dass ich mir auch mal was selber ausdenke und nicht größtenteils nur nochmal 1:1 dasselbe zu programmieren wie bei der Vorlage.
-
-
Soo, ich habe mal versucht, meinen Tokenizer etwas zu verbessern bzw. mit weniger Konstanten zu arbeiten. Außerdem finde ich es jetzt etwas übersichtlicher und einfacher zu verstehen:
Spoiler anzeigen VB.NET-Quellcode
- Imports System.Text
- Public Class Tokenizer
- Public Property Code As String = ""
- Public Property TokenizerInfo As TokenizerInfo = New TokenizerInfo
- Public Property Tokens As List(Of Token()) = New List(Of Token())
- Public Sub Tokenize()
- Debug.WriteLine("----------------------------------------------------------------------")
- 'Den Code in die einzelnen Zeilen zerteilen
- Dim Lines As IEnumerable(Of String) = Code.Split(TokenizerInfo.LineSeperators.ToArray, StringSplitOptions.RemoveEmptyEntries)
- 'Alle Zeilen durchgehen
- Dim Tokens As New List(Of Token())
- For LineIndex As Integer = 0 To Lines.Count - 1
- Dim LineTokens As New List(Of Token)
- Dim Line As String = Lines(LineIndex)
- 'Zeile auseinandernehmen
- Debug.WriteLine(String.Format("Analyzing Line {0}: '{1}'", LineIndex + 1, Line))
- Dim Buffer As New StringBuilder(String.Empty)
- Dim Type As BasicTokenType = BasicTokenType.Undefined
- For CharIndex As Integer = 0 To Line.Length - 1
- Dim CurrentChar As Char = Line(CharIndex)
- 'Prüfen, was der Char ist
- Select Case True
- Case TokenizerInfo.TokenSeperators.Contains(CurrentChar)
- 'Ein Tokentrenner wurde gefunden
- 'Prüfen, ob sich der Car in einem String oder einem Kommentar befindet
- If Not (Type = BasicTokenType.String OrElse Type = BasicTokenType.Comment) Then
- 'Buffer in neuen Token entleeren
- LineTokens.Add(New Token(Buffer.ToString))
- Buffer.Clear()
- Type = BasicTokenType.Undefined
- Else
- 'Nichts machen
- Buffer.Append(CurrentChar)
- End If
- Case CurrentChar = TokenizerInfo.StringIndicator
- 'Ein Stringanfang oder ein Stringende wurde gefunden
- 'Prüfen, ob sich der Car in einem String oder einem Kommentar befindet
- If Type = BasicTokenType.Comment Then
- 'Nichts machen
- Buffer.Append(CurrentChar)
- ElseIf Type = BasicTokenType.String Then
- 'String beenden
- Buffer.Append(CurrentChar)
- LineTokens.Add(New Token(Buffer.ToString))
- Buffer.Clear()
- Type = BasicTokenType.Undefined
- Else
- 'Buffer in neuen Token entleeren und aktuellen Char dem Buffer hinzufügen
- LineTokens.Add(New Token(Buffer.ToString))
- Buffer.Clear()
- Type = BasicTokenType.String
- Buffer.Append(CurrentChar)
- End If
- Case CurrentChar = TokenizerInfo.CommentIndicator
- 'Das Kommentarzeichen wurde gefunden
- If Not (Type = BasicTokenType.String OrElse Type = BasicTokenType.Comment) Then
- 'Buffer in neuen Token entleeren
- LineTokens.Add(New Token(Buffer.ToString))
- Buffer.Clear()
- Type = BasicTokenType.Comment
- Buffer.Append(CurrentChar)
- Else
- 'Nichts machen
- Buffer.Append(CurrentChar)
- End If
- Case CurrentChar = TokenizerInfo.OpenBracket
- 'Eine öffnendee Klammer wurde gefunden
- If Not (Type = BasicTokenType.String OrElse Type = BasicTokenType.Comment) Then
- 'Buffer in neuen Token entleeren
- Buffer.Append(CurrentChar)
- LineTokens.Add(New Token(Buffer.ToString))
- Buffer.Clear()
- Type = BasicTokenType.Undefined
- Else
- 'Nichts machen
- Buffer.Append(CurrentChar)
- End If
- Case CurrentChar = TokenizerInfo.CloseBracket
- 'Eine schließende Klammer wurde gefunden
- If Not (Type = BasicTokenType.String OrElse Type = BasicTokenType.Comment) Then
- If Buffer.Length > 0 Then
- 'Buffer in neuen Token entleeren
- LineTokens.Add(New Token(Buffer.ToString))
- Buffer.Clear()
- End If
- Buffer.Append(CurrentChar)
- LineTokens.Add(New Token(Buffer.ToString))
- Buffer.Clear()
- Type = BasicTokenType.Undefined
- Else
- 'Nichts machen
- Buffer.Append(CurrentChar)
- End If
- Case Else
- 'Ein anderes Zeichen wurde gefunden
- 'Nichts machen
- Buffer.Append(CurrentChar)
- End Select
- Next
- LineTokens.Add(New Token(Buffer.ToString))
- 'Alle leeren Tokens entfernen
- LineTokens = LineTokens.Where(Function(t) t.Code <> String.Empty).ToList
- 'Tokens der aktuellen Zeile abspeichern
- Tokens.Add(LineTokens.ToArray)
- Debug.WriteLine(" Splitted Line in Tokens:")
- Debug.WriteLine(String.Format(" '{0}'", String.Join("' '", LineTokens)))
- Next
- 'Alle Tokens abspeichern
- Me.Tokens = Tokens
- End Sub
- Private Enum BasicTokenType
- [String]
- Comment
- Undefined
- End Enum
- End Class
Dazu gehören halt noch die Klassen Token und TokenizerInfo:
Spoiler anzeigen VB.NET-Quellcode
- Public Class TokenizerInfo
- Public Property OpenBracket As Char = "("c
- Public Property CloseBracket As Char = ")"c
- Public Property StringIndicator As Char = """"c
- Public Property CommentIndicator As Char = "'"c
- Public Property TokenSeperators As IEnumerable(Of String) = {" "}
- Public Property LineSeperators As IEnumerable(Of Char) = {ControlChars.Cr, ControlChars.Lf}
- End Class
Der Sinn der Token-Klasse sollte klar sein, die TokenizerInfo gibt einfach ein paar Zeichen, welche man je nach Anwendung eventuell ändern wollte und fasst diese eben der Übersichtlichkeit wegen zusammen.
Der Output ist soweit ich weiß, immer noch derselbe, nur etwas anders formatiert:
Aus:
wird:
Quellcode
- Analyzing Line 1: 'X <- (sin(cos(19)) + 8) / 10 'Variable zuweisen'
- Splitted Line in Tokens:
- 'X' '<-' '(' 'sin(' 'cos(' '19' ')' ')' '+' '8' ')' '/' '10' ''Variable zuweisen'
- Analyzing Line 2: 'Y <- Input("Hallo Welt, bitte geben sie eine Zahl ein:")'
- Splitted Line in Tokens:
- 'Y' '<-' 'Input(' '"Hallo Welt, bitte geben sie eine Zahl ein:"' ')'
- Analyzing Line 3: ''Jetzt das noch ausgeben'
- Splitted Line in Tokens:
- ''Jetzt das noch ausgeben'
- Analyzing Line 4: 'Output(Y)'
- Splitted Line in Tokens:
- 'Output(' 'Y' ')'
Jetzt fehlt nur noch die Zuordnung zu verschiedenen Typen, das Interpretieren ist dann ne andere Sache :).
PS: Ich habe zwar schon 2 Basistypen für den Tokenizer implementiert, diese speicher ich aber nicht mit, da es mir dann etwas zu unübersichtlich zwischen all den schon zugeordneten und den noch nicht zugeordneten Tokens werden würde. -
Ich hänge gerade daran fest, die Tokens auch zuzuordnen. Mein Plan war, alles so zu machen, dass am Schluss ein großer Haufen verschachtelter Funktionen bleibt. Beispielsweise so:
wirs zu:
Also im Grunde genommen eine Art Prefix-Notation, nur dass jede Klammerebene eine eigene Klasse ist. Ich mach mal eine Art Baumstruktur, vielleicht ist das dann ja übersichtlicher:
Vielleicht versteht ihr ja so, wie ich das gemeint habe :). Im Kopf funktioniert das zwar auch, jedoch habe ich immer mehr Probleme, nicht zuletzt wegen der verschiedenen Datentypen und vor Allem auch wegen den Statements (If, For, ...). Kann mir da vielleicht jemand weiterhelfen? -
-
Du solltest mal versuchen, die Rechnungen so zu unterteilen, dass am Schluss ein Baum mit Rechenoperationen entsteht, die nur 2 Werte haben
Folglich wird aus "b + 9 * (3 - a)"
So hab ich mich mal an einen Formelparser rangemacht, der Variablen und Grundrechenarten unterstützte, wird aber komplex, sobald Funktionen im Ausdruck vorhanden sind.
Übrigends: ein Compiler zerlegt genau so, da die Befehle so besser in Assembler übersetzt werden können. -
@EiPott
Das Problem ist halt, dass ich das gar nicht übersetzen will, sondern danach direkt interpretieren. Dein System ist übrigens genau dasselbe wie das, was ich vorher hatte mit dem einzigsten Unterschied in der Darstellung.
PS: Dein Beispiel ist falsch, Punkt for Strich :P.
Mein Hauptproblem ist, dass ich ja keinen Formelparser sondern einen Parser für komplette Scripte programmieren will. Einen ganz normalen Formelparser zu programmieren ist ja jetzt wirklich nicht so schwer, Codeteile zu parsen, welche sich über mehrere Zeilen usw ziehen finde ich zumindest deutlich schwerer. Außerdem habe ich noch das Problem mit den verschiedenen Typen :S. -
Der Parser an sich funktioniert (zumindest bei deinem einfachen Fall) genauso, wie der für nen einfachen Formelparser. Du musst vielleicht noch so Sachen wie Strings berücksichtigen.
Einzig die Tokens an sich müssen dann aufwändiger behandelt werden, aber das parsen ist davon ja erstmal gar nicht betroffen.