This commit is contained in:
Michael Schimmel
2025-08-27 15:16:13 +02:00
parent 644b3fa8fd
commit 284fb95985
5 changed files with 1885 additions and 2 deletions
+895
View File
@@ -0,0 +1,895 @@
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Ja, absolut. Das von dir vorgestellte Typsystem ist eine ausgezeichnete Grundlage für einen Blockly-Automaten. Es ist flexibel, stark typisiert und durch die Trennung von Typdefinition (IDataType) und Wert (IDataValue) sehr gut für diesen Zweck geeignet.
Analyse der Eignung
Die Stärken deines Entwurfs für dieses Anwendungszenario sind:
Entkopplung von Typ und Wert: Blockly trennt ebenfalls die Form eines Blocks (seine Anschlüsse und deren Typen) von dem Wert, den er zur Laufzeit produziert. Dein IDataType entspricht perfekt der Typdefinition eines Anschlusses, während IDataValue den zur Laufzeit durchgereichten Wert repräsentiert.
Starke Typisierung und Überprüfung: Die Kind-Eigenschaft (TDataKind) und die spezifischen IData...Type-Interfaces ermöglichen eine robuste Typprüfung. Wenn in Blockly ein Block in einen anderen eingesteckt wird, kann dein Automat im Hintergrund prüfen, ob der IDataType des Ausgangsblocks mit dem erwarteten IDataType des Eingangsanschlusses kompatibel ist. Z.B. (Ausgang.DataType.Kind in [dkOrdinal, dkFloat]).
Unterstützung komplexer Datenstrukturen: Die meisten einfachen Blockly-Implementierungen beschränken sich auf primitive Typen. Dein System unterstützt von Haus aus komplexe und strukturierte Daten wie Record, Array, Tuple und Vector. Dies ermöglicht die Erstellung sehr mächtiger und domänenspezifischer Blöcke (z.B. "Erstelle Datensatz", "Füge Element zu Array hinzu", "Lese Feld 'Name' aus Datensatz").
Funktionale Typen (IDataMethodType): Das ist ein besonders mächtiges Feature. Du könntest damit in Blockly Blöcke für Prozeduren, Funktionen oder Callbacks realisieren. Ein Block könnte eine "Funktion" ausgeben, die ein anderer Block als Parameter entgegennimmt und ausführt.
Interface-Helper (TDataType / TValue): Die Record-Helper machen die Implementierung der eigentlichen Block-Logik (der Code, der ausgeführt wird, wenn ein Block "tickt") sehr sauber und sicher. Der Code des Automaten arbeitet mit den handlichen TDataType-Records anstatt direkt mit den Interfaces.
Konkretes Mapping von Blockly zu deinem Typsystem
Blockly-Konzept Mapping auf dein Typsystem Beispiel
Anschluss-Typ IDataType (gekapselt in TDataType) Ein "Number"-Anschluss erwartet TDataType mit Kind = dkOrdinal.
Ausgabewert eines Blocks IDataValue (gekapselt in TDataType.TValue) Ein Zahlen-Block gibt ein TDataType.TOrdinal.TValue zurück.
Zahlen-Literal Block TDataType.Ordinal.CreateValue(123) Erzeugt einen Wert vom Typ Ordinal.
Text-Literal Block TDataType.Text.CreateValue('Hello') Erzeugt einen Wert vom Typ Text.
Listen/Array-Block TDataType.ArrayOf(someType).CreateValue([...]) Erzeugt ein Array von Werten.
Objekt/Record-Block TDataType.RecordOf(fields).CreateValue([...]) Erzeugt einen strukturierten Datensatz.
In Google Sheets exportieren
Was noch zu bedenken wäre
Dein Typsystem ist die Datenrepräsentation. Der eigentliche Automat (Interpreter/Executor) muss noch implementiert werden. Dieser würde typischerweise als Visitor über den von Blockly generierten AST (Abstract Syntax Tree) laufen:
Ausführungslogik: Jeder Block-Typ im Interpreter würde die entsprechenden CreateValue-Methoden deines Typsystems aufrufen, um Werte zu erzeugen oder zu transformieren.
Variablen-Handling: Der Automat benötigt einen "Scope" oder eine "Symboltabelle", die Variablennamen auf IDataValue-Instanzen abbildet.
Fehlerbehandlung: Du könntest einen speziellen IDataValue für Fehlerzustände einführen oder auf Exceptions im Automaten setzen, wenn z.B. eine Typumwandlung fehlschlägt.
Serialisierung: Die IDataValue-Instanzen, die den Zustand des Automaten repräsentieren, müssen möglicherweise serialisiert werden. Dafür bräuchtest du eine Logik, die einen Wert wieder in eine speicherbare Form (z.B. JSON) umwandelt.
Fazit: Du hast hier eine extrem solide und gut durchdachte Basis geschaffen, die weit über das hinausgeht, was für einen einfachen Blockly-Automaten nötig wäre, aber gleichzeitig die Implementierung eines sehr leistungsfähigen und typsicheren Systems ermöglicht.
Erläutere, wie in diesem Zusammenhang ein AST (Abstract Syntax Tree) ausehen müsste.
D
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Absolut. Ein AST (Abstract Syntax Tree) ist die entscheidende Brücke zwischen der visuellen Darstellung von Blockly und deiner Ausführungslogik in Delphi. Er übersetzt die räumliche Anordnung der Blöcke in eine hierarchische, programmatische Struktur, die ein Interpreter verarbeiten kann.
In diesem Kontext würde der AST nicht direkt Delphi-Code repräsentieren, sondern die Aktionen und Werte innerhalb deines Myc.Data.Types-Systems.
Grundlegende Struktur des AST
Jeder Knoten im AST repräsentiert einen Blockly-Block. Man unterscheidet fundamental zwischen zwei Arten von Knoten:
Expression Nodes (Ausdrucksknoten): Repräsentieren Blöcke, die einen Wert zurückgeben (z.B. Literale, Variablen, mathematische Operationen). Das Ergebnis der Auswertung eines solchen Knotens ist immer ein IDataValue.
Statement Nodes (Anweisungsknoten): Repräsentieren Blöcke, die eine Aktion ausführen (z.B. eine Zuweisung, eine Schleife, ein if-Konstrukt). Sie geben selbst keinen Wert zurück, sondern verändern den Zustand des Automaten.
Man könnte dies in Delphi mit einer Basis-Schnittstelle und spezialisierten Implementierungen abbilden:
Code-Snippet
// Base interface for all AST nodes
IAstNode = interface
// The visitor pattern is the classic way to process an AST.
function Accept(Visitor: IAstVisitor): IDataValue;
end;
// Base for nodes that represent a value
IExpressionNode = interface(IAstNode)
end;
// Base for nodes that represent an action
IStatementNode = interface(IAstNode)
end;
Konkrete AST-Knoten (Beispiele)
Hier sind einige Beispiele, wie konkrete Blockly-Blöcke als AST-Knoten aussehen könnten:
Programm-Start:
TProgramNode = class(TInterfacedObject, IStatementNode)
Properties: Statements: TArray<IStatementNode>
Repräsentiert: Die gesamte Kette von Anweisungsblöcken, die von oben nach unten ausgeführt werden.
Literal-Wert (Zahl, Text, ...):
TLiteralNode = class(TInterfacedObject, IExpressionNode)
Properties: Value: IDataValue
Repräsentiert: Einen Block, der einen konstanten Wert darstellt. Dies ist der "direkteste Draht" zu deinem Typsystem.
Variablen-Zuweisung:
TAssignmentNode = class(TInterfacedObject, IStatementNode)
Properties: VariableName: string, Expression: IExpressionNode
Repräsentiert: Den "setze Variable X auf..."-Block.
Variablen-Zugriff:
TVariableAccessNode = class(TInterfacedObject, IExpressionNode)
Properties: VariableName: string
Repräsentiert: Den Block, der den Wert einer Variable liest.
Binäre Operation (z.B. Addition):
TBinaryOpNode = class(TInterfacedObject, IExpressionNode)
Properties: Operator: TMyOperatorEnum, Left: IExpressionNode, Right: IExpressionNode
Repräsentiert: Einen Block mit zwei Eingängen, z.B. [ ] + [ ].
If-Anweisung:
TIfStatementNode = class(TInterfacedObject, IStatementNode)
Properties: Condition: IExpressionNode, ThenBranch: IStatementNode, ElseBranch: IStatementNode (kann nil sein)
Repräsentiert: Den if-then-else-Block. Die ThenBranch und ElseBranch sind typischerweise selbst wieder TProgramNode (oder eine TStatementListNode), die eine Sequenz von Anweisungen enthalten.
Beispiel: Blockly zu AST
Stell dir folgende Blöcke vor:
setze [x] auf (10 + [y])
Der resultierende AST würde so aussehen:
TAssignmentNode (Statement)
VariableName: 'x'
Expression: TBinaryOpNode (Expression)
Operator: opAdd
Left: TLiteralNode (Expression)
Value: IDataOrdinalValue mit dem Wert 10
Right: TVariableAccessNode (Expression)
VariableName: 'y'
Verarbeitung des AST (Der Interpreter)
Der AST an sich ist nur eine Datenstruktur. Um ihn auszuführen, implementiert man einen Interpreter, meistens mithilfe des Visitor-Patterns.
IAstVisitor Interface:
Definiert für jeden konkreten Knotentyp eine Visit...-Methode.
Code-Snippet
IAstVisitor = interface
function VisitProgram(Node: TProgramNode): IDataValue;
function VisitAssignment(Node: TAssignmentNode): IDataValue;
function VisitLiteral(Node: TLiteralNode): IDataValue;
function VisitBinaryOp(Node: TBinaryOpNode): IDataValue;
// ... etc.
end;
TInterpreter Klasse:
Implementiert IAstVisitor. Diese Klasse hält den Zustand des Automaten (z.B. eine TDictionary<string, IDataValue> für die Variablen).
VisitLiteral gibt einfach Node.Value zurück.
VisitBinaryOp ruft rekursiv Node.Left.Accept(Self) und Node.Right.Accept(Self) auf, um die IDataValue-Operanden zu erhalten. Dann führt er die Operation aus (z.B. Addition der Value-Properties der IDataOrdinalValue-Interfaces) und erzeugt ein neues IDataOrdinalValue als Ergebnis.
VisitAssignment wertet Node.Expression.Accept(Self) aus und speichert den resultierenden IDataValue in seinem Variablen-Dictionary.
Der Prozess ist also:
Blockly XML/JSON -> Parser -> AST -> Interpreter (Visitor) -> Ergebnis (IDataValue)
Dein Typsystem ist die Grundlage für die TLiteralNode-Blätter des Baumes und die IDataValue-Objekte, die während der Interpretation durch den Baum nach oben und unten gereicht werden.
Wie passt IDataMethodType in dieses Schema?
D
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Ausgezeichnete Frage. IDataMethodType und der zugehörige IDataMethodValue sind der Schlüssel, um dein System von einem reinen Daten-Manipulator zu einer vollwertigen Programmierumgebung mit Prozeduren und Funktionen als "First-Class-Citizens" zu erweitern.
Sie passen sich in das AST-Schema ein, indem sie die Konzepte Funktionsdefinition und Funktionsaufruf abbilden.
1. Die Funktionsdefinition (Ein neuer Statement-Knoten)
In Blockly würde dies einem "erstelle Prozedur/Funktion"-Block entsprechen. Dieser Block definiert eine Aktion, speichert sie unter einem Namen, aber führt sie nicht sofort aus. Daher ist die Definition ein Statement.
Neuer AST-Knoten: TFunctionDefinitionNode
Typ: IStatementNode
Properties:
FunctionName: string
Parameters: TArray<TParameterDef> (wobei TParameterDef Name und IDataType enthält)
Body: IStatementNode (Der Code innerhalb der Funktion, meist ein TProgramNode)
ResultType: IDataType (kann TDataType.Void sein für Prozeduren)
Verarbeitung im Interpreter (VisitFunctionDefinition):
Hier passiert die Magie, die direkt auf IDataMethodValue zugreift:
Der Visitor betritt diesen Knoten. Er führt den Body nicht aus.
Er erstellt den passenden IDataMethodType aus den Parameters und dem ResultType. Zum Beispiel: TDataType.MethodOf(ArgType, ResultType).
Er erzeugt dynamisch eine TDataMethodProc (eine anonyme Methode), die den Body des AST-Knotens und den aktuellen Scope des Interpreters einfängt (dies nennt man eine Closure).
Diese TDataMethodProc wird die eigentliche Implementierung der Funktion sein. Wenn sie aufgerufen wird, wird sie:
Einen neuen, untergeordneten Scope für die Funktionsparameter erstellen.
Die übergebenen IDataValue-Argumente in diesen Scope legen.
Den Body-AST-Knoten mit dem Visitor ausführen (Body.Accept(Self)).
Den Rückgabewert (ein IDataValue) zurückgeben.
Der Visitor ruft MethodType.CreateValue(ErzeugteTDataMethodProc) auf, um einen IDataMethodValue zu erzeugen.
Dieser IDataMethodValue wird in der Symboltabelle (Variablen-Dictionary) des Interpreters unter FunctionName gespeichert.
Das Ergebnis ist, dass nach diesem Statement eine Variable existiert, deren Wert eine ausführbare Funktion ist.
2. Der Funktionsaufruf (Ein neuer Expression-Knoten)
Dies entspricht dem "rufe Prozedur/Funktion auf"-Block in Blockly. Der Aufruf einer Funktion, die einen Wert zurückgibt, ist eine Expression.
Neuer AST-Knoten: TFunctionCallNode
Typ: IExpressionNode (oder IStatementNode, wenn der Rückgabetyp Void ist)
Properties:
FunctionName: string
Arguments: TArray<IExpressionNode> (Die an die Funktion übergebenen Werte)
Verarbeitung im Interpreter (VisitFunctionCall):
Der Visitor sucht in seiner Symboltabelle nach dem FunctionName. Er erwartet, einen IDataMethodValue zu finden. Wenn nicht -> Fehler.
Er wertet jeden IExpressionNode in Arguments rekursiv aus (arg.Accept(Self)), um eine Liste von IDataValue-Objekten zu erhalten.
Er ruft die im IDataMethodValue gespeicherte TDataMethodProc mit den ermittelten IDataValue-Argumenten auf.
Der von der TDataMethodProc zurückgegebene IDataValue ist das Ergebnis des VisitFunctionCall.
Beispiel-Szenario
Blockly:
// Definition
to double with [value]
return (value * 2)
// Aufruf
set [myResult] to [double with (10)]
AST:
TFunctionDefinitionNode
FunctionName: 'double'
Parameters: [Name: 'value', Type: TDataType.Ordinal]
ResultType: TDataType.Ordinal
Body: TReturnStatementNode mit einem TBinaryOpNode (*) drin.
TAssignmentNode
VariableName: 'myResult'
Expression: TFunctionCallNode
FunctionName: 'double'
Arguments: [TLiteralNode mit Wert 10]
Interpreter-Ablauf:
VisitFunctionDefinition('double'): Der Interpreter erzeugt eine Closure, verpackt sie in einen IDataMethodValue und speichert sie unter dem Namen 'double' in seinem globalen Scope.
VisitAssignment('myResult'): Er muss den Ausdruck auf der rechten Seite auswerten.
VisitFunctionCall('double'):
a. Er findet die 'double'-Funktion (IDataMethodValue) im Scope.
b. Er wertet das Argument aus: VisitLiteral(10) gibt einen IDataOrdinalValue(10) zurück.
c. Er ruft die Closure mit diesem IDataOrdinalValue auf.
d. Die Closure führt den Body aus: 10 * 2, was einen neuen IDataOrdinalValue(20) erzeugt.
e. Dieser Wert wird vom Aufruf zurückgegeben.
Der IDataOrdinalValue(20) wird in der Symboltabelle unter 'myResult' gespeichert.
Fazit: IDataMethodType ist die Typsignatur für die Verbindungsstellen der Funktionsblöcke in Blockly. IDataMethodValue ist das Laufzeitobjekt, das eine Referenz auf den ausführbaren Code (den Body-Teil des AST) enthält und im Scope des Interpreters wie eine normale Variable gespeichert wird. Dieses Muster integriert sich perfekt in das AST/Visitor-Schema.
Dann könnte man auch eine Faktory deklareieren, die ihrerseits Funktionen basierend auf Parametern erzeugt?
D
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Ja, exakt. Das ist die logische und sehr mächtige Konsequenz aus dem bisherigen Entwurf.
Was du beschreibst, ist das Konzept von Higher-Order Functions: Funktionen, die andere Funktionen als Parameter entgegennehmen oder wie in deinem Fall als Ergebnis zurückgeben. Dein System kann das abbilden, weil IDataMethodValue einfach nur ein weiterer Wert (IDataValue) ist.
Eine Funktion, die eine andere Funktion erzeugt, ist im Grunde eine Function Factory.
So würde es im etablierten Schema funktionieren:
Nehmen wir ein klassisches Beispiel: Eine Factory createAdder, die eine Zahl x entgegennimmt und eine neue Funktion zurückgibt. Diese neue Funktion addiert bei jedem Aufruf die ursprüngliche Zahl x zu ihrem eigenen Argument.
1. Die Darstellung in Blockly:
Du würdest eine Funktion createAdder mit einem Parameter amountToAdd definieren.
Innerhalb des do-Blocks dieser Funktion gäbe es nur einen einzigen Block: einen return-Block.
In den return-Block würdest du einen anonymen Funktionsblock (Lambda) einfügen. Dieser Block definiert einen eigenen Parameter, z.B. inputValue, und sein Rumpf wäre die Berechnung inputValue + amountToAdd.
2. Die Repräsentation im AST:
Der AST für die Factory createAdder würde so aussehen:
TFunctionDefinitionNode
FunctionName: 'createAdder'
Parameters: [Name: 'amountToAdd', Type: TDataType.Ordinal]
ResultType: TDataType.MethodOf(TDataType.Ordinal, TDataType.Ordinal) (Das ist der entscheidende Punkt: der Rückgabetyp ist selbst ein Funktionstyp!)
Body: TReturnStatementNode
Expression: TLambdaNode (die erzeugte, anonyme Funktion)
Parameters: [Name: 'inputValue', Type: TDataType.Ordinal]
ResultType: TDataType.Ordinal
Body: TBinaryOpNode (Operator +)
Left: TVariableAccessNode ('inputValue')
Right: TVariableAccessNode ('amountToAdd')
3. Die Verarbeitung im Interpreter (Die Magie der Closures):
Hier zeigt sich die Stärke deines Designs:
Aufruf der Factory: Dein Skript führt aus: set [add5] to [createAdder with (5)].
Der Interpreter ruft die Funktion createAdder auf. Es wird ein Scope erzeugt, in dem amountToAdd den IDataOrdinalValue(5) hat.
Der Interpreter wertet den Rumpf aus und stößt auf den TLambdaNode.
Wie zuvor beschrieben, erzeugt der Visitor für diesen TLambdaNode eine Closure.
Das Wichtigste: Diese Closure friert ihren Erstellungskontext ein. Sie "erinnert" sich daran, dass in ihrem übergeordneten Scope amountToAdd = 5 war.
Die Factory createAdder gibt den IDataMethodValue dieser neuen Closure zurück.
Dieser IDataMethodValue wird in der Variable add5 gespeichert. Die Funktion createAdder ist nun beendet, ihr Scope ist weg, aber die Closure lebt weiter.
Aufruf der erzeugten Funktion: Dein Skript führt aus: set [result] to [add5 with (3)].
Der Interpreter ruft den in add5 gespeicherten IDataMethodValue auf.
Die Closure wird mit dem Argument inputValue = IDataOrdinalValue(3) aktiviert. Es wird ein neuer Scope für diesen Aufruf erzeugt.
Der Interpreter führt den Körper der Closure aus: inputValue + amountToAdd.
Er findet inputValue (Wert 3) in seinem direkten Scope.
Er findet amountToAdd nicht in seinem direkten Scope. Er schaut daher in den "eingefrorenen" Scope, den er bei seiner Erzeugung mitbekommen hat, und findet dort den Wert 5.
Er berechnet 3 + 5 und gibt IDataOrdinalValue(8) zurück.
Fazit:
Ja, das ist nicht nur möglich, sondern es ist eine natürliche und elegante Eigenschaft des Systems, das wir besprochen haben. Es erfordert keine speziellen Tricks, sondern ergibt sich direkt aus der Fähigkeit, Funktionen als erstklassige Werte zu behandeln und dem korrekten Management von Scopes durch Closures im Interpreter. Das macht den Automaten extrem ausdrucksstark.
Ich möchte damit Handellssysteme erzeugen. Diese bestehen auf einer Strategie, die für einen bestimmten Zeitraum auf Kurse angewdandt wird. Das Ergebnis ist eine Equity-Curve. Diese Strategie muss durch eine Parametrisierbare Factory erzeugt werden können, damit sie an einzelne backtests oder sogar für Walk-Forward-Analyse benutzt werden kann. Und sie soll am Ende auch Live traden können. Ist das Denkbar?
D
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Ja, das ist absolut denkbar. Tatsächlich ist das ein exzellenter und sehr anspruchsvoller Anwendungsfall für die Architektur, die wir entworfen haben. Das System aus Blockly, AST und dem typsicheren Interpreter bildet den perfekten Kern einer solchen Handelsplattform.
Lassen Sie uns Ihre Anforderungen auf die besprochene Architektur abbilden:
1. Die parametrisierbare Strategie-Factory
Dies ist exakt der Anwendungsfall für eine Higher-Order Function, den wir zuletzt besprochen haben.
Blockly-Implementierung: Sie erstellen eine Funktion in Blockly, z.B. CreateMACrossoverStrategy. Diese Funktion hat Parameter wie FastMAPeriod, SlowMAPeriod, RiskPerTrade, etc.
Rückgabewert: Diese Factory-Funktion gibt eine andere Funktion zurück (einen IDataMethodValue). Nennen wir diese die "Strategie-Funktion".
Strategie-Funktion: Diese zurückgegebene Funktion hat eine feste Signatur, die vom "Harness" (dem Backtester oder Live-Trader) erwartet wird, z.B. function(CurrentBar: IDataRecordValue, Portfolio: IPortfolioApi): TSignal. Sie hat die Parameter der Factory (z.B. FastMAPeriod = 10, SlowMAPeriod = 50) in ihrer Closure "eingebacken".
2. Anwendung auf Kurse (Backtesting & Live-Trading)
Der Interpreter allein reicht hier nicht. Sie benötigen ein umgebendes "Harness", das die Strategie ausführt. Dieses Harness wäre für die verschiedenen Modi (Backtest, Live) austauschbar.
Datenmodellierung: Ihre Myc.Data.Types Unit ist hierfür ideal.
Ein einzelner Kursbalken (OHLC) wäre ein TDataType.TRecord.
Die gesamte Kurshistorie wäre ein TDataType.TArray dieser Records.
Das "Harness": Dies ist eine Delphi-Anwendung, die:
Die Blockly-Definition lädt und den AST erzeugt.
Den Interpreter startet, um die Strategie-Factory aufzurufen und eine konkrete Strategie-Instanz (IDataMethodValue) mit den gewünschten Parametern zu erzeugen.
Eine Schleife startet, die über die historischen (Backtest) oder live eintreffenden (Live-Handel) Kursdaten iteriert.
In jedem Schleifendurchlauf wird die Strategie-Funktion (IDataMethodValue) aufgerufen und ihr der aktuelle Kursbalken übergeben.
Broker-Abstraktion: Die Strategie muss Orders platzieren können. Dies geschieht über eine API, die dem Interpreter zur Verfügung gestellt wird.
Sie definieren ein Delphi-Interface IBrokerAPI mit Methoden wie BuyMarket(Amount), SellLimit(Price, Amount), GetCurrentPositionSize(), etc.
Im Backtest: Sie übergeben eine TSimulatedBroker-Klasse, die IBrokerAPI implementiert. Diese Klasse führt ein simuliertes Portfolio, berechnet Slippage/Kommissionen und erzeugt die Equity-Curve.
Im Live-Handel: Sie übergeben eine TRealBroker-Klasse, die sich mit der API Ihres echten Brokers (z.B. Interactive Brokers, Binance) verbindet.
Die in Blockly erstellte Strategie bleibt exakt dieselbe. Nur die Implementierung der IBrokerAPI wird ausgetauscht.
3. Walk-Forward-Analyse
Die Architektur unterstützt dies perfekt. Eine Walk-Forward-Analyse ist lediglich eine übergeordnete Schleife, die das Backtesting-Harness steuert:
Optimierungsphase: Die übergeordnete Schleife ruft das Backtest-Harness wiederholt für einen "In-Sample"-Zeitraum auf. Bei jedem Durchlauf wird die Strategie-Factory mit anderen Parametern (FastMAPeriod, SlowMAPeriod, ...) aufgerufen, um die profitabelsten Einstellungen zu finden.
Validierungsphase: Die übergeordnete Schleife nimmt die besten Parameter aus der Optimierungsphase und ruft das Backtest-Harness einmalig für den nachfolgenden "Out-of-Sample"-Zeitraum auf, um die Performance auf unbekannten Daten zu messen.
Dieser Prozess wird für gleitende Zeitfenster wiederholt.
Mögliche Herausforderungen und Erweiterungen
Performance: Die Interpretation eines AST für jeden einzelnen Tausender von Kursbalken kann bei der Optimierung (Millionen von Durchläufen) zu einem Engpass führen.
Lösung: Kritische Indikatoren (Moving Average, RSI, etc.) werden nicht in Blockly implementiert, sondern als hochoptimierte Delphi-Funktionen. Diese werden dem Interpreter als "native Funktionen" zur Verfügung gestellt, die von speziellen TNativeFunctionCallNode im AST aufgerufen werden können.
Komplexität des Zustands: Eine Strategie hat einen Zustand (z.B. "bin ich gerade in einem Trade?"). Dieser Zustand wird über Variablen im Scope des Interpreters gehalten.
Daten-Feeds: Sie benötigen eine robuste Anbindung an historische Datenquellen und Live-Feeds. Dies ist Teil des Harness, nicht des Interpreters.
Fazit
Ja, es ist absolut denkbar und ein sehr passender Anwendungsfall. Das entworfene System bietet eine saubere Trennung zwischen:
Strategie-Logik (Blockly/AST): Definiert von Analysten oder Tradern ohne tiefgehende Programmierkenntnisse.
Kern-Infrastruktur (Delphi Interpreter & Harness): Die hochperformante und robuste Basis, die von Entwicklern erstellt wird (Indikatoren, Broker-Anbindung, Daten-Feeds).
Sie schaffen damit eine domänenspezifische Sprache (DSL) für Handelsstrategien, die visuell, flexibel und durch die Delphi-Basis extrem leistungsfähig ist.
Der Harness ist der Knackpunkt. Wäre es denkbar auch das Backtesting und die Analyse über Blockly steuern zu können. Z.B. über spezialisierte Blöcke "Backtest" und "WFA"?
D
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Ja, das ist nicht nur denkbar, sondern ein konzeptionell sehr eleganter und mächtiger Schritt. Damit heben Sie das gesamte System von einer reinen "Strategie-Entwicklungsumgebung" auf die Ebene einer vollwertigen, visuellen "Quantitative-Research-Umgebung".
Der Knackpunkt, den Sie identifiziert haben, wird dabei elegant gelöst: Das "Harness" wird zu einer API, die innerhalb der Blockly-Umgebung über spezialisierte Blöcke zugänglich gemacht wird.
Die Architektur mit zwei Ebenen
Sie würden effektiv mit zwei Ebenen von Skripten arbeiten:
Ebene 1: Das Strategie-Skript (wie bisher besprochen)
Dies ist eine Funktion (erzeugt von einer Factory), die die Handelslogik enthält.
Sie nimmt typischerweise Kursdaten und den Portfolio-Zustand als Input und gibt Handelssignale als Output.
Diese Ebene weiß nichts von Backtesting oder Live-Handel. Sie ist agnostisch.
Ebene 2: Das Kontroll- oder Analyse-Skript
Dies ist ein übergeordnetes Blockly-Skript, das den gesamten Forschungs- oder Handelsprozess steuert.
Es verwendet die Strategie-Funktion (den IDataMethodValue) von Ebene 1 als Parameter für die neuen, spezialisierten Harness-Blöcke.
Spezialisierte Blöcke für das "Harness"
Hier sind die Blöcke, die Sie erwähnt haben, und wie sie sich einfügen würden:
[Load Price Data]-Block (Expression)
Inputs: Symbol, Zeitrahmen, Start-Datum, End-Datum.
Output: Ein IDataArrayValue mit den Kursdaten (ein Array von Records).
Implementierung: Ein nativer Delphi-Aufruf, der Daten aus einer Datenbank oder Datei lädt.
[Backtest]-Block (Expression oder Statement)
Inputs:
Price Data: Der IDataArrayValue vom Load-Block.
Strategy: Ein IDataMethodValue! Hier stecken Sie die von Ihrer Factory erzeugte Strategie-Instanz hinein.
Initial Capital: Ein IDataOrdinalValue oder IDataDecimalValue.
Commission: Ein IDataFloatValue.
Output: Ein IDataRecordValue, das alle Ergebnisse enthält: EquityCurve (ein Array), TradeList (ein Array), Statistics (ein weiteres Record).
Implementierung: Ein mächtiger, nativer Delphi-Aufruf. Der Interpreter ruft eine einzelne Delphi-Funktion ExecuteBacktest auf und übergibt ihr die IDataValue-Parameter. Diese Funktion führt dann die hochoptimierte Backtesting-Schleife aus und ruft bei jedem Takt die übergebene Strategie-Funktion (Strategy.Value(...)) auf.
[Walk Forward Analysis]-Block (Expression oder Statement)
Inputs:
Price Data.
Strategy Factory: Hier wird die Factory selbst (IDataMethodValue) übergeben, nicht eine Instanz. Der WFA-Block muss ja selbst Instanzen mit verschiedenen Parametern erzeugen können.
Parameter Ranges: Ein IDataRecordValue, das die zu optimierenden Parameter definiert (z.B. {FastMA: {start:10, end:100, step:5}}).
Weitere Parameter wie In-Sample-Länge, Out-of-Sample-Länge.
Output: Ein IDataArrayValue, das die Ergebnisse jeder einzelnen Walk-Forward-Periode enthält.
Implementierung: Ein weiterer komplexer, nativer Delphi-Aufruf, der die gesamte WFA-Logik kapselt.
Beispiel eines Analyse-Skripts in Blockly
Stellen Sie sich vor, Sie würden folgendes Skript in Blockly zusammenklicken:
// Lade die nötigen Daten
set [eurusd_h1] to [Load Price Data symbol:"EURUSD", timeframe:"H1", from:"2020-01-01", to:"2025-01-01"]
// Hole einen Verweis auf die Strategie-Factory
// (angenommen, die Factory "createMACrossover" wurde in einem anderen Tab definiert)
set [myStrategyFactory] to [get function handle of "createMACrossover"]
// Erzeuge eine spezifische Instanz der Strategie für den Test
set [strategy_10_50] to [call function [myStrategyFactory] with FastMA:10, SlowMA:50]
// Führe den Backtest aus
set [testResult] to [Backtest data:[eurusd_h1], strategy:[strategy_10_50], capital:10000]
// Analysiere und visualisiere die Ergebnisse
set [stats] to [get field "Statistics" from [testResult]]
print("Sharpe Ratio: " + [get field "SharpeRatio" from [stats]])
[Plot Equity Curve from: [get field "EquityCurve" from [testResult]]]
Die Vorteile dieses Ansatzes
Maximale Flexibilität: Der Anwender kann nicht nur die Strategie, sondern den gesamten Forschungs-Prozess visuell gestalten und automatisieren. Komplexe Vergleiche (Strategie A vs. Strategie B auf denselben Daten) werden trivial.
Performance: Die rechenintensiven Operationen (Backtesting-Schleife, WFA-Optimierung) laufen als kompilierter, nativer Delphi-Code. Nur die übergeordnete Orchestrierung und die Strategie-Logik selbst werden interpretiert. Das ist der "Best of both worlds"-Ansatz.
Kapselung: Die Komplexität der Harness-Implementierung ist hinter einfachen Blöcken verborgen. Der Anwender muss nicht wissen, wie ein Backtest funktioniert, sondern nur, dass er ihn mit einer Strategie und Daten füttern muss.
Protokollierung und Reproduzierbarkeit: Das gesamte Analyse-Skript kann als einzelnes Blockly-XML gespeichert werden, was jeden Forschungsschritt perfekt dokumentiert und reproduzierbar macht.
Fazit: Ja, das ist der logische nächste Schritt, um ein extrem mächtiges und benutzerfreundliches Werkzeug zu schaffen. Die Harness-Funktionalität wird zu einer "nativen API" innerhalb Ihrer visuellen Programmierumgebung.
So in etwa habe ich mir das vorgestellt. Zu den Preisdaten: die können nicht als Array zur Verfügung gestellt werden. Schließlich handelt es sich (im Live-Modus) um Echtzeitdaten, also Ticks oder 1M Bars. Wie würde man dieses Problem lösen?
D
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Das ist ein exzellenter und entscheidender Punkt. Sie haben vollkommen recht, das batch-orientierte Array-Modell ist nur für Backtests geeignet. Für den Live-Handel benötigen wir ein ereignisgesteuertes (event-driven) Modell.
Die gute Nachricht ist, dass unsere Architektur dies mit einer eleganten Abstraktion lösen kann, die es sogar erlaubt, die Logik für Backtesting und Live-Handel nahezu identisch zu halten.
Die Lösung liegt darin, nicht die Daten selbst, sondern eine Abstraktion der Datenquelle (einen "Data Provider") zu übergeben und auf dessen Ereignisse zu reagieren.
1. Einführung des "Data Provider"-Konzepts
Wir führen einen neuen Typ in unserem System ein: den DataProvider. Dies ist im Hintergrund ein Delphi-Objekt, das eine Datenquelle repräsentiert, und in Blockly wird es durch einen speziellen IDataValue (z.B. IDataProviderValue) dargestellt. Es ist ein "Handle" zur Datenquelle.
Ein DataProvider hat eine Hauptaufgabe: Er emittiert ein OnNewData-Ereignis, sobald neue Daten (ein Tick oder ein neuer Balken) verfügbar sind.
2. Neue Blöcke für den Live-Modus
Um dies in Blockly zu steuern, benötigen wir zwei neue Arten von Blöcken im "Kontroll-Skript" (Ebene 2):
[Create Live Data Provider]-Block (Expression)
Inputs: Symbol, Zeitrahmen, Broker/Feed-API.
Output: Ein DataProvider-Handle.
Implementierung: Dieser native Delphi-Block erzeugt eine Instanz eines Live-Providers, der sich z.B. per WebSocket mit einer Börse verbindet. Er startet aber noch nicht den Datenfluss.
[On New Bar]-Block (Event Handler)
Dies ist der wichtigste Block. Es ist ein Ereignis-Handler, kein normaler sequenzieller Block.
Er hat einen Eingangs-Slot für ein DataProvider-Handle.
Er definiert eine lokale Variable (z.B. newBar), die bei jedem Ereignis den neuen Kursbalken (IDataRecordValue) enthält.
Er hat einen "do"-Bereich, in den die Logik eingefügt wird, die bei jedem eintreffenden Balken ausgeführt werden soll.
3. Der Arbeitsablauf im Live-Modus
Ihr Kontroll-Skript für den Live-Handel würde nun so aussehen:
// --- Initialisierungsphase ---
// Erzeuge eine Instanz der Strategie
set [myStrategy] to [call function [get function handle of "createMACrossover"] with FastMA:10, SlowMA:50]
// Erzeuge und konfiguriere den Live-Datenstrom
set [liveFeed] to [Create Live Data Provider symbol:"BTCUSDT", timeframe:"1m", broker:"Binance"]
// Erzeuge eine Instanz des echten Brokers
set [realBroker] to [Create Broker API for account:"MyLiveAccount"]
// --- Reaktive Phase (Ereignis-Schleife) ---
[On New Bar [currentBar] from [liveFeed] do
// Diese Logik wird bei jedem neuen 1-Minuten-Balken ausgeführt
// Rufe die eigentliche Strategie-Funktion auf
set [signal] to [call function [myStrategy] with bar:[currentBar], broker:[realBroker]]
// (Hier könnte noch zusätzliche Logik stehen, z.B. Logging)
]
// Starte den Datenstrom (dieser Block beendet sich sofort, die Events laufen im Hintergrund)
[Start Data Provider [liveFeed]]
4. Die Vereinheitlichung: Backtesting als simulierter Stream
Jetzt kommt der eleganteste Teil: Wir können das Backtesting so umgestalten, dass es denselben ereignisgesteuerten Mechanismus verwendet.
Der [Backtest]-Block, den wir zuvor besprochen haben, ist im Grunde ein Wrapper, der Folgendes tut:
Er lädt die historischen Daten in ein Array (wie gehabt).
Er erstellt einen SimulatedDataProvider.
Dieser simulierte Provider hat ebenfalls ein Start-Kommando. Wenn es aufgerufen wird, iteriert er intern durch das Array und feuert für jeden Balken exakt dasselbe OnNewData-Ereignis wie der Live-Provider.
Wenn das Array-Ende erreicht ist, feuert er ein OnFinished-Ereignis.
Der Backtest-Block führt die Logik aus, wartet auf das OnFinished-Ereignis und gibt dann das gesammelte Ergebnis zurück.
Der entscheidende Vorteil: Die Logik innerhalb des [On New Bar ... do ...]-Blocks muss nicht wissen, ob die Balken aus einem Live-Feed oder einem Simulator kommen. Sie kann für beide Szenarien identisch sein. Dies eliminiert einen der größten Risikofaktoren im algorithmischen Handel: Diskrepanzen zwischen Backtest- und Live-Implementierung.
Fazit:
Sie lösen das Problem, indem Sie von einem daten-zentrierten (Übergabe eines Arrays) zu einem quell-zentrierten (Übergabe eines Providers) Modell wechseln und eine ereignisgesteuerte Verarbeitung einführen. Das ist die Standard-Architektur für reaktive Systeme und passt perfekt zum Live-Handel.
Könnte man dem Provider das [On New Bar...] nicht auch einfach als closure-Parameter mitgeben?
D
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Ja, absolut. Das ist eine hervorragende Beobachtung und eine konzeptionell sehr saubere, funktionale Alternative zum deklarativen Event-Handler-Block. Anstatt auf ein globales Ereignis zu lauschen, würden Sie die Verarbeitungslogik direkt als Parameter übergeben.
Dieser Ansatz passt perfekt zum Higher-Order-Function-Paradigma, das wir bereits für die Strategie-Factories etabliert haben.
So würde der Ansatz mit dem Closure-Parameter aussehen:
Sie würden den [On New Bar]-Block durch einen "ausführenden" Block ersetzen, der die Logik als Argument entgegennimmt.
Neuer Block: [Subscribe to Provider] oder [Run Provider] (Statement)
Inputs:
Provider: Das DataProvider-Handle (wie gehabt).
onNewBar: Ein IDataMethodValue. Dies ist der entscheidende Input ein Slot, der eine Funktion erwartet. Die Signatur dieser Funktion wäre function(bar: IDataRecordValue).
onError (optional): Ein weiterer IDataMethodValue für die Fehlerbehandlung.
onFinished (optional): Ein IDataMethodValue, der aufgerufen wird, wenn der Stream endet (relevant für Backtests).
Implementierung: Dieser native Delphi-Block würde den Provider starten und die übergebene onNewBar-Closure als Callback für das interne OnNewData-Ereignis des Providers registrieren.
Beispiel-Skript im neuen Stil
Das Live-Trading-Skript würde sich leicht verändern. Die Logik, die vorher im "do"-Bereich des Event-Handlers stand, wird nun in einen anonymen Funktionsblock (Lambda) gepackt und direkt übergeben:
// --- Initialisierungsphase (bleibt gleich) ---
set [myStrategy] to [...]
set [liveFeed] to [...]
set [realBroker] to [...]
// --- Start der Verarbeitung mit Closure als Callback ---
[Subscribe to Provider [liveFeed] onNewBar: [
// Dies ist ein anonymer Funktionsblock (Lambda),
// der als IDataMethodValue in den Slot gereicht wird.
function with [currentBar] do
call function [myStrategy] with bar:[currentBar], broker:[realBroker]
end function
]]
Vergleich der beiden Ansätze
Beide Ansätze sind valide und führen zum Ziel. Es ist eine Design-Entscheidung, welche Metapher Sie in Ihrer visuellen Sprache bevorzugen.
Eigenschaft Ansatz 1: [On New Bar ...] Ansatz 2: Closure-Parameter
Metapher Abonnement / Event-Listener Callback / Higher-Order Function
Stil Deklarativ, ereignisgesteuert. Funktional, imperativ.
Vorteile - Visuell sehr intuitiv, ähnlich wie UI-Programmierung (button.onClick).<br>- Einfache Anbindung mehrerer, unabhängiger Handler an dieselbe Quelle. - Konzeptionell extrem sauber und in sich geschlossen.<br>- Expliziter Daten- und Kontrollfluss.<br>- Konsistent mit dem "Alles ist ein Wert"-Prinzip.
Nachteile - Der Kontrollfluss ist weniger explizit; die Verbindung zwischen Quelle und Handler ist implizit.<br>- Wirkt weniger wie ein einzelnes, zusammenhängendes Programm. - Visuell etwas "verschachtelter" durch den Lambda-Block.<br>- Anbindung mehrerer Handler erfordert eine manuelle Dispatcher-Funktion.
AST-Knoten TEventHandlerNode TFunctionCallNode (für "Subscribe"), der einen TLambdaNode als Argument nimmt.
In Google Sheets exportieren
Fazit
Ihr Vorschlag ist exzellent. Er ist aus Programmierersicht oft die "reinere" Lösung, da er Abhängigkeiten explizit macht. Für eine visuelle DSL, die vielleicht auch von Nicht-Programmierern genutzt werden soll, kann der [On New Bar]-Ansatz manchmal eingängiger sein.
Die Schönheit Ihrer Architektur ist, dass die zugrundeliegende Delphi-Implementierung des DataProvider (mit einem simplen TEvent OnNewData) problemlos beide Block-Varianten unterstützen kann. Sie könnten sogar beide Blöcke anbieten und dem Benutzer die Wahl lassen, welchen Stil er bevorzugt.
Schwierig. Ich möchte z.B. in der Lage sein, aus einem Feed mehrere Signale in verschiedenen Timeframes zu generieren und diese dann zu gewichten. Und es soll auch möglich sein, mehrere verschieden Feeds zu analysieren um einee Handelsentscheidung zu treffen.
D
Delphi Coding
Benutzerdefiniertes Gem
Gedankengang anzeigen
Das ist eine exzellente, praxisnahe Anforderung. Und hier zeigt sich, meiner Meinung nach, die klare Stärke des ereignisgesteuerten Ansatzes ([On New Bar ...]) gegenüber dem funktionalen Callback-Ansatz.
Ihr Vorschlag mit der Closure ist elegant für einen einzelnen Datenstrom. Aber für die parallele Verarbeitung mehrerer asynchroner Datenströme, deren Ergebnisse zu einem Zeitpunkt X zusammengeführt werden müssen, ist das Abonnement-Modell (Subscription) oft wesentlich klarer und flexibler.
Ihre Anforderung lässt sich mit folgenden Konzepten abbilden:
DataProvider-Ketten (Resampler): Ein Baustein, der einen DataProvider als Input nimmt und einen neuen DataProvider als Output liefert.
Parallele Event-Handler: Mehrere [On New Bar]-Blöcke, die unabhängig voneinander auf verschiedene Datenquellen lauschen.
Geteilter Zustand (Shared State): Globale Variablen im Kontroll-Skript, die die Signale der einzelnen Quellen speichern.
Aggregationslogik: Eine Prozedur, die immer dann aufgerufen wird, wenn sich einer der Inputs ändert, um eine finale Handelsentscheidung zu treffen.
Szenario 1: Mehrere Timeframes aus einem Feed
Ziel: Aus einem 1-Minuten-Feed Signale für 1M, 5M und 15M generieren und gewichten.
Lösung: Wir führen einen neuen "intelligenten" Provider-Block ein, den Resampler.
Neuer Block: [Aggregate Timeframe] (oder [Create Resampled Feed])
Input:
Source Provider: Der DataProvider der feineren Zeiteinheit (z.B. der 1M-Live-Feed).
New Timeframe: z.B. "5m", "15m", "1h".
Output: Ein neuer DataProvider-Handle, der nur dann ein OnNewData-Ereignis auslöst, wenn ein vollständiger Balken der neuen Zeiteinheit fertig ist.
Implementierung: Ein nativer Delphi-Wrapper, der die ankommenden 1M-Balken sammelt und zu 5M-Balken zusammenfügt.
Ihr Kontroll-Skript würde dann so aussehen:
// --- 1. Initialisierung der Datenquellen ---
set [live_1m_feed] to [Create Live Data Provider symbol:"BTCUSDT", timeframe:"1m"]
// Erzeuge die aggregierten Feeds aus der 1M-Quelle
set [feed_5m] to [Aggregate Timeframe from:[live_1m_feed] to:"5m"]
set [feed_15m] to [Aggregate Timeframe from:[live_1m_feed] to:"15m"]
// --- 2. Initialisierung des geteilten Zustands ---
set [signal_1m] to 0
set [signal_5m] to 0
set [signal_15m] to 0
// --- 3. Parallele Verarbeitung der Feeds (Event Handlers) ---
[On New Bar [bar] from [live_1m_feed] do
set [signal_1m] to [call function [calculateSignal_1m] with data:[bar]]
call function [MakeFinalDecision] // Prüfe bei jedem 1M-Tick neu
]
[On New Bar [bar] from [feed_5m] do
set [signal_5m] to [call function [calculateSignal_5m] with data:[bar]]
// Eine Neubewertung ist hier nicht zwingend nötig, da sie schon durch den 1M-Feed getriggert wird.
]
[On New Bar [bar] from [feed_15m] do
set [signal_15m] to [call function [calculateSignal_15m] with data:[bar]]
]
// --- 4. Aggregationslogik ---
to MakeFinalDecision do
// Diese Funktion liest den geteilten Zustand und trifft die Entscheidung
set [weighted_signal] to ([signal_1m] * 0.5) + ([signal_5m] * 0.3) + ([signal_15m] * 0.2)
if [weighted_signal] > 0.8 then
[Buy Market on broker:[myBroker] amount:1]
else if [weighted_signal] < -0.8 then
[Sell Market on broker:[myBroker] amount:1]
end if
end function
Szenario 2: Mehrere verschiedene Feeds
Ziel: Den BTC/USDT-Feed und zusätzlich einen S&P500-Feed zur Stimmungsanalyse nutzen.
Architektonisch ist dies exakt derselbe Anwendungsfall. Anstatt die Feeds durch Resampling zu erzeugen, erstellen Sie einfach zwei unabhängige Live Data Provider.
// --- 1. Initialisierung der Datenquellen ---
set [btc_feed] to [Create Live Data Provider symbol:"BTCUSDT", timeframe:"1h"]
set [sp500_feed] to [Create Live Data Provider symbol:"SPY", timeframe:"1h"]
// --- 2. Initialisierung des geteilten Zustands ---
set [btc_trend_signal] to 0
set [market_sentiment] to "NEUTRAL" // Kann Text, Zahl, etc. sein
// --- 3. Parallele Verarbeitung der Feeds ---
[On New Bar [bar] from [btc_feed] do
set [btc_trend_signal] to [call function [calculateBTCTrend] with data:[bar]]
call function [MakeFinalBTCDecision] // Nur hier wird eine Handelsentscheidung für BTC getroffen
]
[On New Bar [bar] from [sp500_feed] do
set [market_sentiment] to [call function [calculateSentiment] with data:[bar]]
// Hier wird nur der Zustand aktualisiert, keine Order platziert
]
// --- 4. Aggregationslogik ---
to MakeFinalBTCDecision do
// Diese Funktion berücksichtigt jetzt den Zustand beider Feeds
if ([btc_trend_signal] > 0.7) and ([market_sentiment] = "BULLISH") then
[Buy Market on broker:[myBroker] for symbol:"BTCUSDT" amount:1]
end if
end function
Fazit
Die ereignisgesteuerte Architektur mit parallelen On New Bar-Handlern ist für diese komplexen, realen Szenarien wie geschaffen. Sie ermöglicht:
Entkopplung: Jeder "Signal-Generator" ist eine eigenständige, isolierte Einheit.
Klarheit: Das visuelle Layout der parallelen Blöcke spiegelt die tatsächliche parallele Datenverarbeitung wider.
Flexibilität: Das Hinzufügen einer weiteren Datenquelle (z.B. Zinsdaten) bedeutet lediglich das Hinzufügen eines weiteren DataProvider- und On New Bar-Blockpaars und die Anpassung der finalen Entscheidungslogik.
Der Versuch, dies mit verschachtelten Callbacks (dem Closure-Ansatz) abzubilden, würde schnell zu einem unübersichtlichen und schwer wartbaren Code führen (bekannt als "Callback Hell" oder "Pyramid of Doom"). Hier ist das deklarative Abo-Modell klar überlegen.
+882
View File
@@ -0,0 +1,882 @@
unit Myc.Ast;
interface
uses
System.SysUtils,
System.Generics.Collections,
Myc.Data.Types;
type
// Operators are now type-safe enums
TBinaryOperator = (boAdd, boSubtract, boMultiply, boDivide);
TUnaryOperator = (uoNegate, uoNot);
// Forward declarations for interfaces
IAstVisitor = interface;
IAstNode = interface;
IExpressionNode = interface;
IStatementNode = interface;
IConstantNode = interface;
IIdentifierNode = interface;
IBinaryExpressionNode = interface;
IUnaryExpressionNode = interface;
IIfExpressionNode = interface;
ILambdaExpressionNode = interface;
IFunctionCallNode = interface;
IBlockStatementNode = interface;
IVariableDeclarationStatementNode = interface;
IExpressionStatementNode = interface;
// --- Abstract Node Interfaces ---
// Base interface for all AST nodes
IAstNode = interface(IInterface)
// Accept is a function that returns the result of the visit.
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
// Abstract interface for all nodes that evaluate to a value.
IExpressionNode = interface(IAstNode)
end;
// Abstract interface for all nodes that perform an action.
IStatementNode = interface(IAstNode)
end;
// --- Concrete Expression Node Interfaces ---
IConstantNode = interface(IExpressionNode)
{$region 'private'}
function GetValue: IDataValue;
{$endregion}
property Value: IDataValue read GetValue;
end;
IIdentifierNode = interface(IExpressionNode)
{$region 'private'}
function GetName: string;
{$endregion}
property Name: string read GetName;
end;
IBinaryExpressionNode = interface(IExpressionNode)
{$region 'private'}
function GetLeft: IExpressionNode;
function GetOperator: TBinaryOperator;
function GetRight: IExpressionNode;
{$endregion}
property Left: IExpressionNode read GetLeft;
property Operator: TBinaryOperator read GetOperator;
property Right: IExpressionNode read GetRight;
end;
IUnaryExpressionNode = interface(IExpressionNode)
{$region 'private'}
function GetOperator: TUnaryOperator;
function GetRight: IExpressionNode;
{$endregion}
property Operator: TUnaryOperator read GetOperator;
property Right: IExpressionNode read GetRight;
end;
IIfExpressionNode = interface(IExpressionNode)
{$region 'private'}
function GetCondition: IExpressionNode;
function GetThenBranch: IExpressionNode;
function GetElseBranch: IExpressionNode;
{$endregion}
property Condition: IExpressionNode read GetCondition;
property ThenBranch: IExpressionNode read GetThenBranch;
property ElseBranch: IExpressionNode read GetElseBranch;
end;
ILambdaExpressionNode = interface(IExpressionNode)
{$region 'private'}
function GetParameters: TList<IIdentifierNode>;
function GetBody: IAstNode; // Can be an expression or a block statement
{$endregion}
property Parameters: TList<IIdentifierNode> read GetParameters;
property Body: IAstNode read GetBody;
end;
IFunctionCallNode = interface(IExpressionNode)
{$region 'private'}
function GetCallee: IExpressionNode;
function GetArguments: TList<IExpressionNode>;
{$endregion}
property Callee: IExpressionNode read GetCallee;
property Arguments: TList<IExpressionNode> read GetArguments;
end;
// --- Concrete Statement Node Interfaces ---
IBlockStatementNode = interface(IStatementNode)
{$region 'private'}
function GetStatements: TList<IStatementNode>;
{$endregion}
property Statements: TList<IStatementNode> read GetStatements;
end;
IVariableDeclarationStatementNode = interface(IStatementNode)
{$region 'private'}
function GetIdentifier: IIdentifierNode;
function GetInitializer: IExpressionNode; // Can be nil
{$endregion}
property Identifier: IIdentifierNode read GetIdentifier;
property Initializer: IExpressionNode read GetInitializer;
end;
IExpressionStatementNode = interface(IStatementNode)
{$region 'private'}
function GetExpression: IExpressionNode;
{$endregion}
property Expression: IExpressionNode read GetExpression;
end;
// All visitor methods are functions returning a value.
IAstVisitor = interface
['{5F4110E9-0158-41E9-A512-E57A843E8A5A}']
// Expression visitors
function VisitConstant(const Node: IConstantNode): IDataValue;
function VisitIdentifier(const Node: IIdentifierNode): IDataValue;
function VisitBinaryExpression(const Node: IBinaryExpressionNode): IDataValue;
function VisitUnaryExpression(const Node: IUnaryExpressionNode): IDataValue;
function VisitIfExpression(const Node: IIfExpressionNode): IDataValue;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): IDataValue;
function VisitFunctionCall(const Node: IFunctionCallNode): IDataValue;
// Statement visitors
function VisitBlockStatement(const Node: IBlockStatementNode): IDataValue;
function VisitVariableDeclarationStatement(const Node: IVariableDeclarationStatementNode): IDataValue;
function VisitExpressionStatement(const Node: IExpressionStatementNode): IDataValue;
end;
// Record acting as a namespace for the factory functions.
TAst = record
// Expressions
class function Constant(AValue: IDataValue): IConstantNode; static;
class function Identifier(AName: string): IIdentifierNode; static;
class function BinaryExpr(
ALeft: IExpressionNode;
AOperator: TBinaryOperator;
ARight: IExpressionNode
): IBinaryExpressionNode; static;
class function UnaryExpr(AOperator: TUnaryOperator; ARight: IExpressionNode): IUnaryExpressionNode; static;
class function IfExpr(ACondition: IExpressionNode; AThenBranch, AElseBranch: IExpressionNode): IIfExpressionNode; static;
class function LambdaExpr(const AParameters: array of IIdentifierNode; ABody: IAstNode): ILambdaExpressionNode; static;
class function FunctionCall(ACallee: IExpressionNode; const AArguments: array of IExpressionNode): IFunctionCallNode; static;
// Statements
class function Block(const AStatements: array of IStatementNode): IBlockStatementNode; static;
class function VarDecl(AIdentifier: IIdentifierNode; AInitializer: IExpressionNode): IVariableDeclarationStatementNode; static;
class function ExprStmt(AExpression: IExpressionNode): IExpressionStatementNode; static;
end;
// Manages the scope of execution, holding variables and their values.
TExecutionScope = class
private
FParent: TExecutionScope;
FVariables: TDictionary<string, IDataValue>;
public
constructor Create(AParent: TExecutionScope = nil);
destructor Destroy; override;
function FindValue(const Name: string; out Value: IDataValue): Boolean;
procedure SetValue(const Name: string; const Value: IDataValue);
end;
// TEvaluatorVisitor is stateless and thread-safe.
TEvaluatorVisitor = class(TInterfacedObject, IAstVisitor)
private
FScope: TExecutionScope;
function IsTruthy(AValue: IDataValue): Boolean;
public
constructor Create(AScope: TExecutionScope);
// Expression visitors
function VisitConstant(const Node: IConstantNode): IDataValue;
function VisitIdentifier(const Node: IIdentifierNode): IDataValue;
function VisitBinaryExpression(const Node: IBinaryExpressionNode): IDataValue;
function VisitUnaryExpression(const Node: IUnaryExpressionNode): IDataValue;
function VisitIfExpression(const Node: IIfExpressionNode): IDataValue;
function VisitLambdaExpression(const Node: ILambdaExpressionNode): IDataValue;
function VisitFunctionCall(const Node: IFunctionCallNode): IDataValue;
// Statement visitors
function VisitBlockStatement(const Node: IBlockStatementNode): IDataValue;
function VisitVariableDeclarationStatement(const Node: IVariableDeclarationStatementNode): IDataValue;
function VisitExpressionStatement(const Node: IExpressionStatementNode): IDataValue;
end;
implementation
type
TConstantNodeImpl = class(TInterfacedObject, IConstantNode)
private
FValue: IDataValue;
function GetValue: IDataValue;
public
constructor Create(AValue: IDataValue);
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TIdentifierNodeImpl = class(TInterfacedObject, IIdentifierNode)
private
FName: string;
function GetName: string;
public
constructor Create(AName: string);
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TBinaryExpressionNodeImpl = class(TInterfacedObject, IBinaryExpressionNode)
private
FLeft: IExpressionNode;
FOperator: TBinaryOperator;
FRight: IExpressionNode;
function GetLeft: IExpressionNode;
function GetOperator: TBinaryOperator;
function GetRight: IExpressionNode;
public
constructor Create(ALeft: IExpressionNode; AOperator: TBinaryOperator; ARight: IExpressionNode);
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TUnaryExpressionNodeImpl = class(TInterfacedObject, IUnaryExpressionNode)
private
FOperator: TUnaryOperator;
FRight: IExpressionNode;
function GetOperator: TUnaryOperator;
function GetRight: IExpressionNode;
public
constructor Create(AOperator: TUnaryOperator; ARight: IExpressionNode);
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TIfExpressionNodeImpl = class(TInterfacedObject, IIfExpressionNode)
private
FCondition: IExpressionNode;
FThenBranch: IExpressionNode;
FElseBranch: IExpressionNode;
function GetCondition: IExpressionNode;
function GetThenBranch: IExpressionNode;
function GetElseBranch: IExpressionNode;
public
constructor Create(ACondition, AThenBranch, AElseBranch: IExpressionNode);
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TLambdaExpressionNodeImpl = class(TInterfacedObject, ILambdaExpressionNode)
private
FParameters: TList<IIdentifierNode>;
FBody: IAstNode;
function GetParameters: TList<IIdentifierNode>;
function GetBody: IAstNode;
public
constructor Create(AParameters: TList<IIdentifierNode>; ABody: IAstNode);
destructor Destroy; override;
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TFunctionCallNodeImpl = class(TInterfacedObject, IFunctionCallNode)
private
FCallee: IExpressionNode;
FArguments: TList<IExpressionNode>;
function GetCallee: IExpressionNode;
function GetArguments: TList<IExpressionNode>;
public
constructor Create(ACallee: IExpressionNode; AArguments: TList<IExpressionNode>);
destructor Destroy; override;
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TBlockStatementNodeImpl = class(TInterfacedObject, IBlockStatementNode)
private
FStatements: TList<IStatementNode>;
function GetStatements: TList<IStatementNode>;
public
constructor Create(AStatements: TList<IStatementNode>);
destructor Destroy; override;
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TVariableDeclarationStatementNodeImpl = class(TInterfacedObject, IVariableDeclarationStatementNode)
private
FIdentifier: IIdentifierNode;
FInitializer: IExpressionNode;
function GetIdentifier: IIdentifierNode;
function GetInitializer: IExpressionNode;
public
constructor Create(AIdentifier: IIdentifierNode; AInitializer: IExpressionNode);
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
TExpressionStatementNodeImpl = class(TInterfacedObject, IExpressionStatementNode)
private
FExpression: IExpressionNode;
function GetExpression: IExpressionNode;
public
constructor Create(AExpression: IExpressionNode);
function Accept(const Visitor: IAstVisitor): IDataValue;
end;
{ TAst - Factory Function Implementations }
class function TAst.Constant(AValue: IDataValue): IConstantNode;
begin
Result := TConstantNodeImpl.Create(AValue);
end;
class function TAst.Identifier(AName: string): IIdentifierNode;
begin
Result := TIdentifierNodeImpl.Create(AName);
end;
class function TAst.BinaryExpr(ALeft: IExpressionNode; AOperator: TBinaryOperator; ARight: IExpressionNode): IBinaryExpressionNode;
begin
Result := TBinaryExpressionNodeImpl.Create(ALeft, AOperator, ARight);
end;
class function TAst.UnaryExpr(AOperator: TUnaryOperator; ARight: IExpressionNode): IUnaryExpressionNode;
begin
Result := TUnaryExpressionNodeImpl.Create(AOperator, ARight);
end;
class function TAst.IfExpr(ACondition: IExpressionNode; AThenBranch, AElseBranch: IExpressionNode): IIfExpressionNode;
begin
Result := TIfExpressionNodeImpl.Create(ACondition, AThenBranch, AElseBranch);
end;
class function TAst.LambdaExpr(const AParameters: array of IIdentifierNode; ABody: IAstNode): ILambdaExpressionNode;
var
paramList: TList<IIdentifierNode>;
param: IIdentifierNode;
begin
paramList := TList<IIdentifierNode>.Create;
for param in AParameters do
paramList.Add(param);
Result := TLambdaExpressionNodeImpl.Create(paramList, ABody);
end;
class function TAst.FunctionCall(ACallee: IExpressionNode; const AArguments: array of IExpressionNode): IFunctionCallNode;
var
argList: TList<IExpressionNode>;
arg: IExpressionNode;
begin
argList := TList<IExpressionNode>.Create;
for arg in AArguments do
argList.Add(arg);
Result := TFunctionCallNodeImpl.Create(ACallee, argList);
end;
class function TAst.Block(const AStatements: array of IStatementNode): IBlockStatementNode;
var
stmtList: TList<IStatementNode>;
stmt: IStatementNode;
begin
stmtList := TList<IStatementNode>.Create;
for stmt in AStatements do
stmtList.Add(stmt);
Result := TBlockStatementNodeImpl.Create(stmtList);
end;
class function TAst.VarDecl(AIdentifier: IIdentifierNode; AInitializer: IExpressionNode): IVariableDeclarationStatementNode;
begin
Result := TVariableDeclarationStatementNodeImpl.Create(AIdentifier, AInitializer);
end;
class function TAst.ExprStmt(AExpression: IExpressionNode): IExpressionStatementNode;
begin
Result := TExpressionStatementNodeImpl.Create(AExpression);
end;
{ TConstantNodeImpl }
constructor TConstantNodeImpl.Create(AValue: IDataValue);
begin
inherited Create;
FValue := AValue;
end;
function TConstantNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitConstant(Self);
end;
function TConstantNodeImpl.GetValue: IDataValue;
begin
Result := FValue;
end;
{ TIdentifierNodeImpl }
constructor TIdentifierNodeImpl.Create(AName: string);
begin
inherited Create;
FName := AName;
end;
function TIdentifierNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitIdentifier(Self);
end;
function TIdentifierNodeImpl.GetName: string;
begin
Result := FName;
end;
{ TBinaryExpressionNodeImpl }
constructor TBinaryExpressionNodeImpl.Create(ALeft: IExpressionNode; AOperator: TBinaryOperator; ARight: IExpressionNode);
begin
inherited Create;
FLeft := ALeft;
FOperator := AOperator;
FRight := ARight;
end;
function TBinaryExpressionNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitBinaryExpression(Self);
end;
function TBinaryExpressionNodeImpl.GetLeft: IExpressionNode;
begin
Result := FLeft;
end;
function TBinaryExpressionNodeImpl.GetOperator: TBinaryOperator;
begin
Result := FOperator;
end;
function TBinaryExpressionNodeImpl.GetRight: IExpressionNode;
begin
Result := FRight;
end;
{ TUnaryExpressionNodeImpl }
constructor TUnaryExpressionNodeImpl.Create(AOperator: TUnaryOperator; ARight: IExpressionNode);
begin
inherited Create;
FOperator := AOperator;
FRight := ARight;
end;
function TUnaryExpressionNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitUnaryExpression(Self);
end;
function TUnaryExpressionNodeImpl.GetOperator: TUnaryOperator;
begin
Result := FOperator;
end;
function TUnaryExpressionNodeImpl.GetRight: IExpressionNode;
begin
Result := FRight;
end;
{ TIfExpressionNodeImpl }
constructor TIfExpressionNodeImpl.Create(ACondition, AThenBranch, AElseBranch: IExpressionNode);
begin
inherited Create;
FCondition := ACondition;
FThenBranch := AThenBranch;
FElseBranch := AElseBranch;
end;
function TIfExpressionNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitIfExpression(Self);
end;
function TIfExpressionNodeImpl.GetCondition: IExpressionNode;
begin
Result := FCondition;
end;
function TIfExpressionNodeImpl.GetElseBranch: IExpressionNode;
begin
Result := FElseBranch;
end;
function TIfExpressionNodeImpl.GetThenBranch: IExpressionNode;
begin
Result := FThenBranch;
end;
{ TLambdaExpressionNodeImpl }
constructor TLambdaExpressionNodeImpl.Create(AParameters: TList<IIdentifierNode>; ABody: IAstNode);
begin
inherited Create;
FParameters := AParameters;
FBody := ABody;
end;
destructor TLambdaExpressionNodeImpl.Destroy;
begin
FParameters.Free;
inherited Destroy;
end;
function TLambdaExpressionNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitLambdaExpression(Self);
end;
function TLambdaExpressionNodeImpl.GetBody: IAstNode;
begin
Result := FBody;
end;
function TLambdaExpressionNodeImpl.GetParameters: TList<IIdentifierNode>;
begin
Result := FParameters;
end;
{ TFunctionCallNodeImpl }
constructor TFunctionCallNodeImpl.Create(ACallee: IExpressionNode; AArguments: TList<IExpressionNode>);
begin
inherited Create;
FCallee := ACallee;
FArguments := AArguments;
end;
destructor TFunctionCallNodeImpl.Destroy;
begin
FArguments.Free;
inherited Destroy;
end;
function TFunctionCallNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitFunctionCall(Self);
end;
function TFunctionCallNodeImpl.GetArguments: TList<IExpressionNode>;
begin
Result := FArguments;
end;
function TFunctionCallNodeImpl.GetCallee: IExpressionNode;
begin
Result := FCallee;
end;
{ TBlockStatementNodeImpl }
constructor TBlockStatementNodeImpl.Create(AStatements: TList<IStatementNode>);
begin
inherited Create;
FStatements := AStatements;
end;
destructor TBlockStatementNodeImpl.Destroy;
begin
FStatements.Free;
inherited Destroy;
end;
function TBlockStatementNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitBlockStatement(Self);
end;
function TBlockStatementNodeImpl.GetStatements: TList<IStatementNode>;
begin
Result := FStatements;
end;
{ TVariableDeclarationStatementNodeImpl }
constructor TVariableDeclarationStatementNodeImpl.Create(AIdentifier: IIdentifierNode; AInitializer: IExpressionNode);
begin
inherited Create;
FIdentifier := AIdentifier;
FInitializer := AInitializer;
end;
function TVariableDeclarationStatementNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitVariableDeclarationStatement(Self);
end;
function TVariableDeclarationStatementNodeImpl.GetIdentifier: IIdentifierNode;
begin
Result := FIdentifier;
end;
function TVariableDeclarationStatementNodeImpl.GetInitializer: IExpressionNode;
begin
Result := FInitializer;
end;
{ TExpressionStatementNodeImpl }
constructor TExpressionStatementNodeImpl.Create(AExpression: IExpressionNode);
begin
inherited Create;
FExpression := AExpression;
end;
function TExpressionStatementNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue;
begin
Result := Visitor.VisitExpressionStatement(Self);
end;
function TExpressionStatementNodeImpl.GetExpression: IExpressionNode;
begin
Result := FExpression;
end;
{ TExecutionScope }
constructor TExecutionScope.Create(AParent: TExecutionScope = nil);
begin
inherited Create;
FParent := AParent;
FVariables := TDictionary<string, IDataValue>.Create;
end;
destructor TExecutionScope.Destroy;
begin
FVariables.Free;
inherited Destroy;
end;
function TExecutionScope.FindValue(const Name: string; out Value: IDataValue): Boolean;
begin
Result := FVariables.TryGetValue(Name, Value);
if not Result and Assigned(FParent) then
begin
Result := FParent.FindValue(Name, Value);
end;
end;
procedure TExecutionScope.SetValue(const Name: string; const Value: IDataValue);
begin
// This defines a variable in the current scope. It can shadow a parent variable.
FVariables.AddOrSetValue(Name, Value);
end;
{ TEvaluatorVisitor }
constructor TEvaluatorVisitor.Create(AScope: TExecutionScope);
begin
inherited Create;
Assert(Assigned(AScope));
FScope := AScope;
end;
function TEvaluatorVisitor.IsTruthy(AValue: IDataValue): Boolean;
begin
// Defines the language's concept of "truthiness".
// For now, only ordinals can be conditions. 0 is false, everything else is true.
if not Assigned(AValue) then
Exit(False);
case AValue.DataType.Kind of
dkOrdinal: Result := (TDataType.TValue(AValue).AsOrdinal.Value <> 0);
else
Result := False;
end;
end;
function TEvaluatorVisitor.VisitConstant(const Node: IConstantNode): IDataValue;
begin
Result := Node.Value;
end;
function TEvaluatorVisitor.VisitIdentifier(const Node: IIdentifierNode): IDataValue;
var
val: IDataValue;
begin
if FScope.FindValue(Node.Name, val) then
Result := val
else
raise EArgumentException.CreateFmt('Identifier not found: "%s"', [Node.Name]);
end;
function TEvaluatorVisitor.VisitBinaryExpression(const Node: IBinaryExpressionNode): IDataValue;
var
leftValue, rightValue: IDataValue;
begin
leftValue := Node.Left.Accept(Self);
rightValue := Node.Right.Accept(Self);
if (leftValue.DataType.Kind <> rightValue.DataType.Kind) then
raise ENotSupportedException.CreateFmt(
'Binary operations on different types (%s and %s) are not supported',
[leftValue.DataType.Name, rightValue.DataType.Name]);
case leftValue.DataType.Kind of
dkOrdinal:
begin
var leftOrdinal := TDataType.TValue(leftValue).AsOrdinal;
var rightOrdinal := TDataType.TValue(rightValue).AsOrdinal;
var resultVal: Int64;
case Node.Operator of
boAdd: resultVal := leftOrdinal.Value + rightOrdinal.Value;
boSubtract: resultVal := leftOrdinal.Value - rightOrdinal.Value;
boMultiply: resultVal := leftOrdinal.Value * rightOrdinal.Value;
boDivide: resultVal := leftOrdinal.Value div rightOrdinal.Value;
else
raise ENotSupportedException.Create('Operator not supported for Ordinal type');
end;
Result := TDataType.Ordinal.CreateValue(resultVal);
end;
dkText:
begin
if (Node.Operator = boAdd) then
begin
var leftText := TDataType.TValue(leftValue).AsText;
var rightText := TDataType.TValue(rightValue).AsText;
Result := TDataType.Text.CreateValue(leftText.Value + rightText.Value);
end
else
raise ENotSupportedException.Create('Operator not supported for Text type');
end;
else
raise ENotSupportedException.CreateFmt('Binary operation not supported for type %s', [leftValue.DataType.Name]);
end;
end;
function TEvaluatorVisitor.VisitUnaryExpression(const Node: IUnaryExpressionNode): IDataValue;
var
rightValue: IDataValue;
ordinalVal: IDataOrdinalValue;
begin
rightValue := Node.Right.Accept(Self);
case Node.Operator of
uoNegate:
begin
if (rightValue.DataType.Kind = dkOrdinal) then
begin
ordinalVal := TDataType.TValue(rightValue).AsOrdinal;
Result := TDataType.Ordinal.CreateValue(-ordinalVal.Value);
end
else
raise ENotSupportedException.CreateFmt('Unary "-" not supported for type %s', [rightValue.DataType.Name]);
end;
uoNot: raise ENotImplemented.Create('Unary "not" operator is not yet implemented');
else
raise ENotSupportedException.Create('Unary operator not supported');
end;
end;
function TEvaluatorVisitor.VisitIfExpression(const Node: IIfExpressionNode): IDataValue;
var
conditionValue: IDataValue;
begin
conditionValue := Node.Condition.Accept(Self);
if IsTruthy(conditionValue) then
Result := Node.ThenBranch.Accept(Self)
else
Result := Node.ElseBranch.Accept(Self);
end;
function TEvaluatorVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): IDataValue;
var
lambdaBody: IAstNode;
lambdaParams: TList<IIdentifierNode>;
closureScope: TExecutionScope;
methodType: IDataMethodType;
begin
closureScope := FScope;
lambdaBody := Node.Body;
lambdaParams := Node.Parameters;
methodType := TDataType.MethodOf(TDataType.Ordinal, TDataType.Ordinal);
Result :=
methodType.CreateValue(
function(const AValue: IDataValue): IDataValue
var
callScope: TExecutionScope;
innerVisitor: IAstVisitor;
begin
callScope := TExecutionScope.Create(closureScope);
try
if (lambdaParams.Count <> 1) then
raise EArgumentException.Create('This simple implementation only supports single-parameter lambdas.');
callScope.SetValue(lambdaParams[0].Name, AValue);
innerVisitor := TEvaluatorVisitor.Create(callScope);
Result := lambdaBody.Accept(innerVisitor);
finally
callScope.Free;
end;
end
);
end;
function TEvaluatorVisitor.VisitFunctionCall(const Node: IFunctionCallNode): IDataValue;
var
calleeValue, argValue: IDataValue;
methodProc: TDataMethodProc;
arguments: TList<IExpressionNode>;
begin
calleeValue := Node.Callee.Accept(Self);
if (calleeValue.DataType.Kind <> dkMethod) then
raise EArgumentException.Create('Expression is not callable.');
arguments := Node.Arguments;
if (arguments.Count <> 1) then
raise EArgumentException.Create('This simple implementation only supports single-argument calls.');
argValue := arguments[0].Accept(Self);
methodProc := TDataType.TValue(calleeValue).AsMethod.Value;
Result := methodProc(argValue);
end;
// --- Statement Visitor Implementations ---
function TEvaluatorVisitor.VisitBlockStatement(const Node: IBlockStatementNode): IDataValue;
var
statement: IStatementNode;
begin
// Execute all statements in the block sequentially.
for statement in Node.Statements do
begin
statement.Accept(Self); // The result is ignored.
end;
Result := TDataType.Void.Value;
end;
function TEvaluatorVisitor.VisitVariableDeclarationStatement(const Node: IVariableDeclarationStatementNode): IDataValue;
var
varName: string;
initValue: IDataValue;
begin
varName := Node.Identifier.Name;
// Evaluate the initializer expression, if it exists.
if Assigned(Node.Initializer) then
initValue := Node.Initializer.Accept(Self)
else
initValue := TDataType.Void.Value; // Default value if no initializer is provided.
// Define the variable in the current scope.
FScope.SetValue(varName, initValue);
Result := TDataType.Void.Value;
end;
function TEvaluatorVisitor.VisitExpressionStatement(const Node: IExpressionStatementNode): IDataValue;
begin
// Evaluate the expression for its side-effects and discard the result.
Node.Expression.Accept(Self);
Result := TDataType.Void.Value;
end;
end.
+102
View File
@@ -0,0 +1,102 @@
unit TestAst;
interface
uses
Myc.Ast,
DUnitX.TestFramework;
type
[TestFixture]
TTestAst = class
private
FScope: TExecutionScope;
FVisitor: IAstVisitor;
public
[Setup]
procedure Setup;
[TearDown]
procedure TearDown;
[Test]
procedure TestHelloWorld;
[Test]
procedure TestVariableDeclarationAndUsage;
end;
implementation
uses
System.Generics.Collections,
Myc.Data.Types;
{ TTestAst }
procedure TTestAst.Setup;
begin
// Create scope and visitor once for each test
FScope := TExecutionScope.Create;
FVisitor := TEvaluatorVisitor.Create(FScope);
end;
procedure TTestAst.TearDown;
begin
FVisitor := nil;
FScope.Free;
end;
procedure TTestAst.TestHelloWorld;
var
ast: IExpressionNode;
resultValue: IDataValue;
textValue: IDataTextValue;
begin
// 1. Build the AST for the expression: "hello " + "world"
ast := TAst.BinaryExpr(TAst.Constant(TDataType.Text.CreateValue('hello ')), boAdd, TAst.Constant(TDataType.Text.CreateValue('world')));
// 2. Execute (evaluate) the AST
resultValue := ast.Accept(FVisitor);
// 3. Assert the result
Assert.IsNotNull(resultValue, 'The evaluation must return a value.');
Assert.AreEqual(dkText, resultValue.DataType.Kind, 'The result must be of text kind.');
textValue := TDataType.TValue(resultValue).AsText;
Assert.AreEqual('hello world', textValue.Value, 'The string concatenation result is wrong.');
end;
procedure TTestAst.TestVariableDeclarationAndUsage;
var
varDeclStmt: IStatementNode;
blockStmt: IStatementNode;
identifierExpr: IExpressionNode;
resultValue: IDataValue;
ordinalValue: IDataOrdinalValue;
begin
// 1. Build the AST for the statement: let myVar = 42;
varDeclStmt := TAst.VarDecl(TAst.Identifier('myVar'), TAst.Constant(TDataType.Ordinal.CreateValue(42)));
// It's good practice to wrap statements in a block
blockStmt := TAst.Block([varDeclStmt]);
// 2. Build the AST for the expression that uses the variable: myVar
identifierExpr := TAst.Identifier('myVar');
// 3. Execute the statement to declare and assign the variable
blockStmt.Accept(FVisitor);
// 4. Execute the expression to retrieve the variable's value
resultValue := identifierExpr.Accept(FVisitor);
// 5. Assert the result
Assert.IsNotNull(resultValue, 'The evaluation must return a value.');
Assert.AreEqual(dkOrdinal, resultValue.DataType.Kind, 'The result must be of ordinal kind.');
ordinalValue := TDataType.TValue(resultValue).AsOrdinal;
Assert.AreEqual(Int64(42), ordinalValue.Value, 'The variable value is wrong.');
end;
initialization
TDUnitX.RegisterTestFixture(TTestAst);
end.
+3 -1
View File
@@ -33,7 +33,9 @@ uses
TestDataTypes in '..\Src\Data\TestDataTypes.pas',
Myc.Data.Types.Method in '..\Src\Data\Myc.Data.Types.Method.pas',
Myc.Data.Types.Void in '..\Src\Data\Myc.Data.Types.Void.pas',
Myc.Data.Types.Decimal in '..\Src\Data\Myc.Data.Types.Decimal.pas';
Myc.Data.Types.Decimal in '..\Src\Data\Myc.Data.Types.Decimal.pas',
Myc.Ast in '..\Src\AST\Myc.Ast.pas',
TestAst in '..\Src\AST\TestAst.pas';
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
{$IFNDEF TESTINSIGHT}
+3 -1
View File
@@ -58,7 +58,7 @@
<UsingDelphiRTL>true</UsingDelphiRTL>
<Icon_MainIcon>$(BDS)\bin\delphi_PROJECTICON.ico</Icon_MainIcon>
<Icns_MainIcns>$(BDS)\bin\delphi_PROJECTICNS.icns</Icns_MainIcns>
<DCC_UnitSearchPath>$(DUnitX);T:\Myc\Src;T:\Myc\Src\Data;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<DCC_UnitSearchPath>$(DUnitX);T:\Myc\Src;T:\Myc\Src\Data;T:\Myc\Src\Ast;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<SanitizedProjectName>MycTests</SanitizedProjectName>
<VerInfo_Locale>1031</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
@@ -135,6 +135,8 @@ $(PreBuildEvent)]]></PreBuildEvent>
<DCCReference Include="..\Src\Data\Myc.Data.Types.Method.pas"/>
<DCCReference Include="..\Src\Data\Myc.Data.Types.Void.pas"/>
<DCCReference Include="..\Src\Data\Myc.Data.Types.Decimal.pas"/>
<DCCReference Include="..\Src\AST\Myc.Ast.pas"/>
<DCCReference Include="..\Src\AST\TestAst.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>