Files
MycLib/ASTPlayground/MainForm.pas
T
Michael Schimmel d731048b41 AST Debugger
2025-09-13 20:40:18 +02:00

725 lines
26 KiB
ObjectPascal

unit MainForm;
interface
uses
System.SysUtils,
System.Types,
System.TypInfo,
System.UITypes,
System.Classes,
System.Variants,
System.Generics.Collections,
System.Math,
FMX.Types,
FMX.Controls,
FMX.Forms,
FMX.Graphics,
FMX.Dialogs,
FMX.Memo.Types,
FMX.StdCtrls,
FMX.ScrollBox,
FMX.Memo,
FMX.Controls.Presentation,
Myc.Ast.Visualizer,
Myc.Data.Scalar,
Myc.Data.Value,
Myc.Ast.Nodes,
Myc.Ast,
Myc.Ast.Evaluator,
Myc.Ast.Printer,
FMX.Layouts,
FMX.Objects,
Myc.Ast.Scope,
Myc.Ast.Debugger;
type
// A test record
TOHLCV = record
Timestamp: TDateTime;
Open: Double;
High: Double;
Low: Double;
Close: Double;
Volume: Int64;
end;
TForm1 = class(TForm)
Panel1: TPanel;
Memo1: TMemo;
Test1Button: TButton;
Test2Button: TButton;
PrettyPrintButton: TButton;
RecursionButton: TButton;
ShowScopeBox: TCheckBox;
FibonacciButton: TButton;
CrerateTriggerExampleButton: TButton;
DoTriggerButton: TButton;
DoTrigger2Button: TButton;
Panel2: TPanel;
ClearButton: TButton;
FlowOnlyBox: TCheckBox;
SeriesTestButton: TButton;
OHLCButton: TButton;
DebugBox: TCheckBox;
FromJSONButton: TButton;
ToJSONButton: TButton;
ExternalFuncButton: TButton;
procedure ClearButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure CreateTriggerExampleButtonClick(Sender: TObject);
procedure DoTrigger2ButtonClick(Sender: TObject);
procedure DoTriggerButtonClick(Sender: TObject);
procedure ExternalFuncButtonClick(Sender: TObject);
procedure FibonacciButtonClick(Sender: TObject);
procedure OHLCButtonClick(Sender: TObject);
procedure PrettyPrintButtonClick(Sender: TObject);
procedure RecursionButtonClick(Sender: TObject);
procedure SeriesTestButtonClick(Sender: TObject);
procedure Test1ButtonClick(Sender: TObject);
procedure Test2ButtonClick(Sender: TObject);
procedure FromJSONButtonClick(Sender: TObject);
procedure ToJSONButtonClick(Sender: TObject);
private
// Stores the last AST generated by Test1 or Test2 button clicks.
FLastAst: IAstNode;
FGScope: IExecutionScope;
FWorkspace: TAuraWorkspace;
procedure WorkspaceMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single);
function CreateVisitor(Scope: IExecutionScope): IAstVisitor;
// Helper function to encapsulate the Bind -> Evaluate pattern
function ExecuteAst(const ANode: IAstNode; const AParentScope: IExecutionScope): TDataValue;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
Myc.Data.Scalar.JSON,
Myc.Data.Decimal,
System.Diagnostics, // For TStopwatch
Myc.Ast.Json; // For TAstJson serialization
{$R *.fmx}
procedure TForm1.ClearButtonClick(Sender: TObject);
begin
FWorkspace.DeleteChildren;
FWorkspace.Repaint;
// Create and prepare the global scope once
FGScope := TExecutionScope.Create(nil);
RegisterNativeFunctions(FGScope);
end;
function TForm1.ExecuteAst(const ANode: IAstNode; const AParentScope: IExecutionScope): TDataValue;
begin
// This helper function handles simple, one-off script executions.
// It binds the AST and then decides whether to run a debug session or a standard evaluation.
var scriptScope := TAst.Bind(ANode, AParentScope);
var visitor := CreateVisitor(scriptScope);
Result := ANode.Accept(visitor);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FWorkspace := TAuraWorkspace.Create(Panel2);
FWorkspace.Parent := Panel2;
FWorkspace.Align := TAlignLayout.Client;
FWorkspace.ClipChildren := true;
FWorkspace.OnMouseDown := WorkspaceMouseDown;
ClearButtonClick(Self);
end;
procedure TForm1.FibonacciButtonClick(Sender: TObject);
var
root: IAstNode;
result: TDataValue;
sw: TStopwatch;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- Recursive fib with AST---');
sw := TStopwatch.StartNew;
root :=
TAst.Block(
[
TAst.VarDecl(
TAst.Identifier('fib'),
TAst.LambdaExpr(
[TAst.Identifier('n')],
TAst.TernaryExpr(
TAst.BinaryExpr(TAst.Identifier('n'), boLess, TAst.Constant(TScalar.FromInt64(2))),
TAst.Identifier('n'),
TAst.BinaryExpr(
TAst.FunctionCall(
TAst.Identifier('Self'),
[TAst.BinaryExpr(TAst.Identifier('n'), boSubtract, TAst.Constant(TScalar.FromInt64(1)))]
),
boAdd,
TAst.FunctionCall(
TAst.Identifier('Self'),
[TAst.BinaryExpr(TAst.Identifier('n'), boSubtract, TAst.Constant(TScalar.FromInt64(2)))]
)
)
)
)
),
TAst.FunctionCall(TAst.Identifier('fib'), [TAst.Constant(TScalar.FromInt64(30))])
]
);
FLastAst := root;
// Execute with FGScope as parent. The helper function handles debug/prod switching.
result := ExecuteAst(root, FGScope);
sw.Stop;
Memo1.Lines.Add(Format('Result: fib(30) %s (calculated in %d ms)', [result.ToString, sw.ElapsedMilliseconds]));
end;
procedure TForm1.PrettyPrintButtonClick(Sender: TObject);
var
visitor: TPrettyPrintVisitor;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- AST Pretty Print ---');
if not Assigned(FLastAst) then
begin
Memo1.Lines.Add('No AST has been generated yet.');
exit;
end;
visitor := TPrettyPrintVisitor.Create;
FLastAst.Accept(visitor);
Memo1.Lines.Add(visitor.GetResult);
end;
procedure TForm1.RecursionButtonClick(Sender: TObject);
var
root: IAstNode;
result: TDataValue;
sw: TStopwatch;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- Recursive factorial(20) ---');
sw := TStopwatch.StartNew;
root :=
TAst.Block(
[
TAst.VarDecl(
TAst.Identifier('factorial'),
TAst.LambdaExpr(
[TAst.Identifier('n')],
TAst.TernaryExpr(
TAst.BinaryExpr(TAst.Identifier('n'), boLess, TAst.Constant(TScalar.FromInt64(2))),
TAst.Constant(TScalar.FromInt64(1)),
TAst.BinaryExpr(
TAst.Identifier('n'),
boMultiply,
TAst.FunctionCall(
TAst.Identifier('Self'),
[TAst.BinaryExpr(TAst.Identifier('n'), boSubtract, TAst.Constant(TScalar.FromInt64(1)))]
)
)
)
)
),
TAst.FunctionCall(TAst.Identifier('factorial'), [TAst.Constant(TScalar.FromInt64(20))])
]
);
FLastAst := root;
// Execute with FGScope as parent.
result := ExecuteAst(root, FGScope);
sw.Stop;
Memo1.Lines.Add(Format('Result: %s (calculated in %d ms)', [result.ToString, sw.ElapsedMilliseconds]));
end;
procedure TForm1.SeriesTestButtonClick(Sender: TObject);
var
ast, callAst: IAstNode;
resultValue: TDataValue;
series: TScalarRecordSeries;
recordDef: TScalarRecordDefinition;
i: Integer;
scope: IExecutionScope;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- Series Test ---');
scope := TExecutionScope.Create(FGScope);
recordDef := TRttiAstHelper.JsonToRecordDefinition(TRttiAstHelper.RecordDefinitionToJson<TOHLCV>);
series := TScalarRecordSeries.Create(recordDef);
for i := 0 to 4 do
begin
series.Add(
TScalarRecord.Create(
recordDef,
[
TScalarValue.FromDateTime(Now + i),
TScalarValue.FromDouble(100.0 + i),
TScalarValue.FromDouble(105.0 + i),
TScalarValue.FromDouble(98.0 + i),
TScalarValue.FromDouble(102.0 + i),
TScalarValue.FromInt64(10000 * (i + 1))
]
)
);
end;
scope.Define('ohlcvSeries', TDataValue.FromRecordSeries(series));
ast :=
TAst.LambdaExpr(
[],
TAst.Block(
[
TAst.VarDecl(
TAst.Identifier('closeColumn'),
TAst.MemberAccess(TAst.Identifier('ohlcvSeries'), TAst.Identifier('Close'))
),
TAst.Indexer(TAst.Identifier('closeColumn'), TAst.Constant(TScalar.FromInt64(1)))
]
)
);
FLastAst := ast;
callAst := TAst.FunctionCall(ast, []);
resultValue := ExecuteAst(callAst, scope);
Memo1.Lines.Add(Format('Result of script: %s', [resultValue.ToString]));
end;
procedure TForm1.Test1ButtonClick(Sender: TObject);
var
main, callAst: IAstNode;
result: TDataValue;
sw: TStopwatch;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- Simple AST Execution ---');
sw := TStopwatch.StartNew;
main :=
TAst.LambdaExpr(
[],
TAst.Block(
[
TAst.VarDecl(TAst.Identifier('a'), TAst.Constant(TScalar.FromInt64(10))),
TAst.VarDecl(
TAst.Identifier('b'),
TAst.BinaryExpr(TAst.Identifier('a'), boMultiply, TAst.Constant(TScalar.FromInt64(2)))
),
TAst.BinaryExpr(TAst.Identifier('a'), boAdd, TAst.Identifier('b'))
]
)
);
FLastAst := main;
callAst := TAst.FunctionCall(main, []);
result := ExecuteAst(callAst, FGScope);
sw.Stop;
Memo1.Lines.Add(Format('Result: %s (calculated in %d ms)', [result.ToString, sw.ElapsedMilliseconds]));
end;
procedure TForm1.Test2ButtonClick(Sender: TObject);
var
root: IAstNode;
result: TDataValue;
sw: TStopwatch;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- Factory Pattern Demo ---');
sw := TStopwatch.StartNew;
root :=
TAst.Block(
[
TAst.VarDecl(
TAst.Identifier('createStrategyInstance'),
TAst.LambdaExpr(
[TAst.Identifier('offset')],
TAst.Block(
[
TAst.VarDecl(TAst.Identifier('baseValue'), TAst.Constant(TScalar.FromInt64(100))),
TAst.BinaryExpr(TAst.Identifier('baseValue'), boAdd, TAst.Identifier('offset'))
]
)
)
),
TAst.FunctionCall(TAst.Identifier('createStrategyInstance'), [TAst.Constant(TScalar.FromInt64(20))]),
TAst.FunctionCall(TAst.Identifier('createStrategyInstance'), [TAst.Constant(TScalar.FromInt64(55))])
]
);
FLastAst := root;
result := ExecuteAst(root, FGScope);
sw.Stop;
Memo1.Lines.Add(Format('Result of the final expression: %s (calculated in %d ms)', [result.ToString, sw.ElapsedMilliseconds]));
end;
procedure TForm1.WorkspaceMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
if Button <> TMouseButton.mbMiddle then
exit;
if FLastAst <> nil then
begin
var visu := TVisualizationMode.vmDetailed;
if FlowOnlyBox.IsChecked then
visu := TVisualizationMode.vmControlFlow;
FWorkspace.BuildTree(FLastAst, TPointF.Create(X, Y), visu);
end;
end;
procedure TForm1.OHLCButtonClick(Sender: TObject);
const
numRecs = 1000;
lookback = 50;
smaSlowLength = 20;
smaFastLength = 10;
var
scope: IExecutionScope;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add(Format('--- Simulating O(1) SMA Crossover Strategy for %d ticks ---', [numRecs]));
var sw := TStopwatch.StartNew;
var setupAst :=
TAst.Block(
[
TAst.VarDecl(
TAst.Identifier('CreateSMA'),
TAst.LambdaExpr(
[TAst.Identifier('len')],
TAst.Block(
[
TAst.VarDecl(TAst.Identifier('sum'), TAst.Constant(TScalar.FromDouble(0.0))),
TAst.VarDecl(TAst.Identifier('count'), TAst.Constant(TScalar.FromInt64(0))),
TAst.LambdaExpr(
[TAst.Identifier('series'), TAst.Identifier('val')],
TAst.Block(
[
TAst.Assign(
TAst.Identifier('sum'),
TAst.BinaryExpr(TAst.Identifier('sum'), boAdd, TAst.Identifier('val'))
),
TAst.Assign(
TAst.Identifier('count'),
TAst.BinaryExpr(TAst.Identifier('count'), boAdd, TAst.Constant(TScalar.FromInt64(1)))
),
TAst.IfExpr(
TAst.BinaryExpr(TAst.Identifier('count'), boGreater, TAst.Identifier('len')),
TAst.Assign(
TAst.Identifier('sum'),
TAst.BinaryExpr(
TAst.Identifier('sum'),
boSubtract,
TAst.Indexer(TAst.Identifier('series'), TAst.Identifier('len'))
)
),
nil
),
TAst.BinaryExpr(
TAst.Identifier('sum'),
boDivide,
TAst.TernaryExpr(
TAst.BinaryExpr(TAst.Identifier('count'), boLess, TAst.Identifier('len')),
TAst.Identifier('count'),
TAst.Identifier('len')
)
)
]
)
)
]
)
)
),
TAst.VarDecl(
TAst.Identifier('smaFast'),
TAst.FunctionCall(TAst.Identifier('CreateSMA'), [TAst.Constant(TScalar.FromInt64(smaFastLength))])
),
TAst.VarDecl(
TAst.Identifier('smaSlow'),
TAst.FunctionCall(TAst.Identifier('CreateSMA'), [TAst.Constant(TScalar.FromInt64(smaSlowLength))])
),
TAst.VarDecl(
TAst.Identifier('maCrossStrategy'),
TAst.LambdaExpr(
[TAst.Identifier('ohlcv')],
TAst.Block(
[
TAst.VarDecl(
TAst.Identifier('closeSeries'),
TAst.MemberAccess(TAst.Identifier('ohlcv'), TAst.Identifier('Close'))
),
TAst.VarDecl(
TAst.Identifier('currentClose'),
TAst.Indexer(TAst.Identifier('closeSeries'), TAst.Constant(TScalar.FromInt64(0)))
),
TAst.VarDecl(
TAst.Identifier('valSmaFast'),
TAst.FunctionCall(
TAst.Identifier('smaFast'),
[TAst.Identifier('closeSeries'), TAst.Identifier('currentClose')]
)
),
TAst.VarDecl(
TAst.Identifier('valSmaSlow'),
TAst.FunctionCall(
TAst.Identifier('smaSlow'),
[TAst.Identifier('closeSeries'), TAst.Identifier('currentClose')]
)
),
TAst.TernaryExpr(
TAst.BinaryExpr(TAst.Identifier('valSmaFast'), boGreater, TAst.Identifier('valSmaSlow')),
TAst.Constant(TScalar.FromInt64(1)),
TAst.Constant(TScalar.FromInt64(-1))
)
]
)
)
)
]
);
FLastAst := setupAst;
// 1. Bind and execute the setup script.
scope := TAst.Bind(setupAst, FGScope);
// This is a temporary visitor just for the setup execution.
var setupVisitor := CreateVisitor(scope);
setupAst.Accept(setupVisitor);
// 2. Prepare for the simulation loop by modifying the now-populated scope.
scope.Define('current_series', TDataValue.Void);
var currentSeriesIdent := TAst.Identifier('current_series');
var callAst := TAst.FunctionCall(TAst.Identifier('maCrossStrategy'), [currentSeriesIdent]);
// 3. Re-bind the scope with the new AST. This creates the FINAL scope for the loop.
scope := TAst.Bind(callAst, scope);
var seriesAddress := currentSeriesIdent.Address;
var visitor := CreateVisitor(scope);
// 5. Simulation Loop
Memo1.Lines.Add('Starting simulation...');
Application.ProcessMessages;
var lastClose := 1000.0;
Randomize;
var recDef := TRttiAstHelper.JsonToRecordDefinition(TRttiAstHelper.RecordDefinitionToJson<TOHLCV>);
var series := TDataValue.FromRecordSeries(TScalarRecordSeries.Create(recDef));
var nw := Now;
var ohlcvRec: TOHLCV;
for var i := 1 to numRecs do
begin
// ... (simulation logic is unchanged) ...
ohlcvRec.Timestamp := nw + (i / (24 * 60));
ohlcvRec.Open := lastClose + (Random * 0.05);
ohlcvRec.Close := lastClose + (Random - 0.49) * 2;
ohlcvRec.High := Max(ohlcvRec.Open, ohlcvRec.Close) + Random;
ohlcvRec.Low := Min(ohlcvRec.Open, ohlcvRec.Close) - Random;
ohlcvRec.Volume := RandomRange(1000, 50000);
lastClose := ohlcvRec.Close;
var recordValue :=
TScalarRecord.Create(
recDef,
[
TScalarValue.FromDateTime(ohlcvRec.Timestamp),
TScalarValue.FromDouble(ohlcvRec.Open),
TScalarValue.FromDouble(ohlcvRec.High),
TScalarValue.FromDouble(ohlcvRec.Low),
TScalarValue.FromDouble(ohlcvRec.Close),
TScalarValue.FromInt64(ohlcvRec.Volume)
]
);
series.AsRecordSeries.Value.Add(recordValue, lookback);
if series.AsRecordSeries.Value.TotalCount >= smaSlowLength then
begin
scope[seriesAddress].Value := series;
var resultValue := callAst.Accept(visitor);
if i mod 50 = 0 then
begin
Memo1.Lines.Add(Format('Tick %d/%d: Close = %.2f, Signal = %s', [i, numRecs, ohlcvRec.Close, resultValue.ToString]));
Application.ProcessMessages;
end;
end;
end;
sw.Stop;
Memo1.Lines.Add('--- Simulation Finished ---');
Memo1.Lines.Add(Format('Total time: %d ms', [sw.ElapsedMilliseconds]));
end;
var
TriggerScope: IExecutionScope;
procedure TForm1.CreateTriggerExampleButtonClick(Sender: TObject);
var
blk: IAstNode;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- Creating Trigger Blueprint ---');
blk :=
TAst.Block(
[
TAst.VarDecl(TAst.Identifier('X'), TAst.Constant(TScalar.FromInt64(0))),
TAst.VarDecl(
TAst.Identifier('tickHandler'),
TAst.LambdaExpr(
[TAst.Identifier('summand')],
TAst.Assign(TAst.Identifier('X'), TAst.BinaryExpr(TAst.Identifier('X'), boAdd, TAst.Identifier('summand')))
)
)
]
);
TriggerScope := TAst.Bind(blk, FGScope);
// This case is simple enough to just inline the logic from ExecuteAst
var visitor := CreateVisitor(TriggerScope);
blk.Accept(visitor);
FLastAst := blk;
Memo1.Lines.Add('Variable "X" and function "tickHandler" defined in persistent scope.');
Memo1.Lines.Add('Click "Do Trigger" to execute.');
end;
function TForm1.CreateVisitor(Scope: IExecutionScope): IAstVisitor;
begin
if DebugBox.IsChecked then
Result := TDebugEvaluatorVisitor.CreateVisitor(Scope, Memo1.Lines, ShowScopeBox.IsChecked)
else
Result := TEvaluatorVisitor.Create(Scope);
end;
procedure TForm1.DoTriggerButtonClick(Sender: TObject);
var
callAst: IFunctionCallNode;
begin
callAst := TAst.FunctionCall(TAst.Identifier('tickHandler'), [TAst.Constant(TScalar.FromInt64(1))]);
FLastAst := callAst;
var X := ExecuteAst(callAst, TriggerScope);
Memo1.Lines.Add(Format('Tick(1)! New value of X: %s', [X.ToString]));
end;
procedure TForm1.DoTrigger2ButtonClick(Sender: TObject);
var
callAst: IFunctionCallNode;
begin
callAst := TAst.FunctionCall(TAst.Identifier('tickHandler'), [TAst.Constant(TScalar.FromInt64(2))]);
FLastAst := callAst;
var X := ExecuteAst(callAst, TriggerScope);
Memo1.Lines.Add(Format('Tick(2)! New value of X: %s', [X.ToString]));
end;
procedure TForm1.ExternalFuncButtonClick(Sender: TObject);
var
scope: IExecutionScope;
callAst: IAstNode;
resultValue: TDataValue;
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('--- Calling external Delphi function from AST ---');
scope := TExecutionScope.Create(FGScope);
scope.Define(
'delphiAdd',
function(const ArgNodes: TArray<TDataValue>): TDataValue
var
val1, val2: Int64;
begin
if Length(ArgNodes) <> 2 then
raise Exception.Create('delphiAdd requires exactly 2 arguments.');
val1 := ArgNodes[0].AsScalar.Value.AsInt64;
val2 := ArgNodes[1].AsScalar.Value.AsInt64;
Result := TScalar.FromInt64(val1 + val2);
end
);
callAst :=
TAst.FunctionCall(TAst.Identifier('delphiAdd'), [TAst.Constant(TScalar.FromInt64(100)), TAst.Constant(TScalar.FromInt64(23))]);
FLastAst := callAst;
resultValue := ExecuteAst(callAst, scope);
Memo1.Lines.Add(Format('Result from delphiAdd(100, 23): %s', [resultValue.ToString]));
end;
procedure TForm1.FromJSONButtonClick(Sender: TObject);
var
jsonString: string;
begin
Memo1.Lines.BeginUpdate;
try
jsonString := Memo1.Lines.Text;
Memo1.Lines.Clear;
if jsonString.IsEmpty then
begin
Memo1.Lines.Add('Memo is empty. Please paste an AST JSON string.');
exit;
end;
try
FLastAst := TAstJson.Deserialize(jsonString);
Memo1.Lines.Add('AST deserialized successfully from JSON.');
Memo1.Lines.Add('You can now visualize it (Middle Mouse Click) or pretty-print it.');
except
on E: Exception do
begin
FLastAst := nil;
Memo1.Lines.Add('Error deserializing AST from JSON:');
Memo1.Lines.Add(E.Message);
Memo1.Lines.Add('--- Original JSON ---');
Memo1.Lines.Text := Memo1.Lines.Text + sLineBreak + jsonString;
end;
end;
finally
Memo1.Lines.EndUpdate;
end;
end;
procedure TForm1.ToJSONButtonClick(Sender: TObject);
var
jsonString: string;
begin
Memo1.Lines.Clear;
if not Assigned(FLastAst) then
begin
Memo1.Lines.Add('No AST available to serialize. Please generate one first.');
exit;
end;
try
jsonString := TAstJson.Serialize(FLastAst);
Memo1.Lines.Text := jsonString;
except
on E: Exception do
begin
Memo1.Lines.Add('Error serializing AST to JSON:');
Memo1.Lines.Add(E.Message);
end;
end;
end;
end.