From d731048b419daf13a80ce4cbc4c65be2cb621b7e Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Sat, 13 Sep 2025 20:40:18 +0200 Subject: [PATCH] AST Debugger --- ASTPlayground/ASTPlayground.dproj | 2 +- ASTPlayground/MainForm.pas | 182 ++++++++++-------------------- Src/AST/Myc.Ast.Debugger.pas | 113 ++++++------------- Src/AST/Myc.Ast.Evaluator.pas | 52 ++++++++- 4 files changed, 143 insertions(+), 206 deletions(-) diff --git a/ASTPlayground/ASTPlayground.dproj b/ASTPlayground/ASTPlayground.dproj index b988ee1..4b373f7 100644 --- a/ASTPlayground/ASTPlayground.dproj +++ b/ASTPlayground/ASTPlayground.dproj @@ -4,7 +4,7 @@ 20.3 FMX True - Release + Debug Win64 ASTPlayground 2 diff --git a/ASTPlayground/MainForm.pas b/ASTPlayground/MainForm.pas index 9650752..ee31f8b 100644 --- a/ASTPlayground/MainForm.pas +++ b/ASTPlayground/MainForm.pas @@ -31,7 +31,7 @@ uses FMX.Layouts, FMX.Objects, Myc.Ast.Scope, - Myc.Ast.Debugger; // Added for TDebugSession + Myc.Ast.Debugger; type // A test record @@ -86,6 +86,7 @@ type 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 @@ -116,31 +117,12 @@ begin end; function TForm1.ExecuteAst(const ANode: IAstNode; const AParentScope: IExecutionScope): TDataValue; -var - scriptScope: IExecutionScope; 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. - scriptScope := TAst.Bind(ANode, AParentScope); - - if DebugBox.IsChecked then - begin - // Debug path: Create, run, and destroy a debug session. - var session := TDebugSession.Create(ShowScopeBox.IsChecked); - try - Result := session.Execute(ANode, scriptScope); - Memo1.Lines.Add('--- DEBUG LOG ---'); - Memo1.Lines.AddStrings(session.Log); - finally - session.Free; - end; - end - else - begin - // Production path: Create and use the standard evaluator. - var visitor := TEvaluatorVisitor.Create(scriptScope); - Result := ANode.Accept(visitor); - end; + var scriptScope := TAst.Bind(ANode, AParentScope); + var visitor := CreateVisitor(scriptScope); + Result := ANode.Accept(visitor); end; procedure TForm1.FormCreate(Sender: TObject); @@ -407,9 +389,7 @@ const smaSlowLength = 20; smaFastLength = 10; var - visitor: IAstVisitor; scope: IExecutionScope; - session: TDebugSession; begin Memo1.Lines.Clear; Memo1.Lines.Add(Format('--- Simulating O(1) SMA Crossover Strategy for %d ticks ---', [numRecs])); @@ -518,25 +498,8 @@ begin scope := TAst.Bind(setupAst, FGScope); // This is a temporary visitor just for the setup execution. - var setupVisitor: IAstVisitor; - if DebugBox.IsChecked then - begin - session := TDebugSession.Create(ShowScopeBox.IsChecked); - try - // Execute setup script within a temporary session. - session.Execute(setupAst, scope); - Memo1.Lines.Add('--- DEBUG LOG (Setup) ---'); - Memo1.Lines.AddStrings(session.Log); - session.Log.Clear; // Clear log for the next phase - finally - session.Free; - end; - end - else - begin - setupVisitor := TEvaluatorVisitor.Create(scope); - setupAst.Accept(setupVisitor); - end; + 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); @@ -547,73 +510,57 @@ begin scope := TAst.Bind(callAst, scope); var seriesAddress := currentSeriesIdent.Address; - // 4. NOW create the visitor for the simulation loop, using the final scope. - session := nil; - try - if DebugBox.IsChecked then - begin - // Create a new session that will live for the duration of the loop. - session := TDebugSession.Create(ShowScopeBox.IsChecked); - visitor := TDebugEvaluatorVisitor.Create(scope, session.Log, ShowScopeBox.IsChecked, 1); - end - else - begin - visitor := TEvaluatorVisitor.Create(scope); - end; + var visitor := CreateVisitor(scope); - // 5. Simulation Loop - Memo1.Lines.Add('Starting simulation...'); - Application.ProcessMessages; + // 5. Simulation Loop + Memo1.Lines.Add('Starting simulation...'); + Application.ProcessMessages; - var lastClose := 1000.0; - Randomize; - var recDef := TRttiAstHelper.JsonToRecordDefinition(TRttiAstHelper.RecordDefinitionToJson); - var series := TDataValue.FromRecordSeries(TScalarRecordSeries.Create(recDef)); - var nw := Now; - var ohlcvRec: TOHLCV; - for var i := 1 to numRecs do + var lastClose := 1000.0; + Randomize; + var recDef := TRttiAstHelper.JsonToRecordDefinition(TRttiAstHelper.RecordDefinitionToJson); + 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 - // ... (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 + scope[seriesAddress].Value := series; + var resultValue := callAst.Accept(visitor); + if i mod 50 = 0 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; + Memo1.Lines.Add(Format('Tick %d/%d: Close = %.2f, Signal = %s', [i, numRecs, ohlcvRec.Close, resultValue.ToString])); + Application.ProcessMessages; end; end; - - sw.Stop; - Memo1.Lines.Add('--- Simulation Finished ---'); - Memo1.Lines.Add(Format('Total time: %d ms', [sw.ElapsedMilliseconds])); - finally - if Assigned(session) then - session.Free; end; + + sw.Stop; + Memo1.Lines.Add('--- Simulation Finished ---'); + Memo1.Lines.Add(Format('Total time: %d ms', [sw.ElapsedMilliseconds])); end; var @@ -622,7 +569,6 @@ var procedure TForm1.CreateTriggerExampleButtonClick(Sender: TObject); var blk: IAstNode; - visitor: IAstVisitor; begin Memo1.Lines.Clear; Memo1.Lines.Add('--- Creating Trigger Blueprint ---'); @@ -643,22 +589,8 @@ begin TriggerScope := TAst.Bind(blk, FGScope); // This case is simple enough to just inline the logic from ExecuteAst - if DebugBox.IsChecked then - begin - var session := TDebugSession.Create(ShowScopeBox.IsChecked); - try - session.Execute(blk, TriggerScope); - Memo1.Lines.Add('--- DEBUG LOG ---'); - Memo1.Lines.AddStrings(session.Log); - finally - session.Free; - end; - end - else - begin - visitor := TEvaluatorVisitor.Create(TriggerScope); - blk.Accept(visitor); - end; + var visitor := CreateVisitor(TriggerScope); + blk.Accept(visitor); FLastAst := blk; @@ -666,6 +598,14 @@ begin 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; diff --git a/Src/AST/Myc.Ast.Debugger.pas b/Src/AST/Myc.Ast.Debugger.pas index 1f3ec2c..8254ddb 100644 --- a/Src/AST/Myc.Ast.Debugger.pas +++ b/Src/AST/Myc.Ast.Debugger.pas @@ -13,26 +13,6 @@ uses Myc.Ast.Evaluator; type - TDebugSession = class - private - class var - FCurrent: TDebugSession; - private - FLog: TStrings; - FShowScope: Boolean; - FVisitorStack: TStack; - function GetLog: TStrings; - public - constructor Create(AShowScope: Boolean); - destructor Destroy; override; - - class property Current: TDebugSession read FCurrent; - - function Execute(const ANode: IAstNode; const AScriptScope: IExecutionScope): TDataValue; - property Log: TStrings read GetLog; - property VisitorStack: TStack read FVisitorStack; - end; - TDebugEvaluatorVisitor = class(TEvaluatorVisitor) private FLog: TStrings; @@ -44,9 +24,18 @@ type procedure ShowScope; protected // Overridden to provide the debug-specific visitor factory. - function GetVisitorFactory: TVisitorFactory; override; + function CreateVisitorFactory: TVisitorFactory; override; + public constructor Create(const AScope: IExecutionScope; ALog: TStrings; AShowScope: Boolean; AInitialIndent: Integer = 0); + + class function CreateVisitor( + const AScope: IExecutionScope; + ALog: TStrings; + AShowScope: Boolean; + AInitialIndent: Integer = 0 + ): IAstVisitor; + function VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; override; // The logging overrides for other Visit... methods function VisitConstant(const Node: IConstantNode): TDataValue; override; @@ -71,44 +60,9 @@ uses System.TypInfo, Myc.Ast.Printer; -{ TDebugSession } - -constructor TDebugSession.Create(AShowScope: Boolean); -begin - inherited Create; - if Assigned(FCurrent) then - raise EInvalidOpException.Create('Another debug session is already active.'); - - FLog := TStringList.Create; - FShowScope := AShowScope; - FVisitorStack := TStack.Create; - FCurrent := Self; -end; - -destructor TDebugSession.Destroy; -begin - FLog.Free; - FVisitorStack.Free; - FCurrent := nil; - inherited Destroy; -end; - -function TDebugSession.Execute(const ANode: IAstNode; const AScriptScope: IExecutionScope): TDataValue; -var - initialVisitor: IAstVisitor; -begin - initialVisitor := TDebugEvaluatorVisitor.Create(AScriptScope, FLog, FShowScope, 0); - Result := ANode.Accept(initialVisitor); -end; - -function TDebugSession.GetLog: TStrings; -begin - Result := FLog; -end; - { TDebugEvaluatorVisitor } -constructor TDebugEvaluatorVisitor.Create(const AScope: IExecutionScope; ALog: TStrings; AShowScope: Boolean; AInitialIndent: Integer); +constructor TDebugEvaluatorVisitor.Create(const AScope: IExecutionScope; ALog: TStrings; AShowScope: Boolean; AInitialIndent: Integer = 0); begin inherited Create(AScope); Assert(Assigned(ALog)); @@ -118,41 +72,32 @@ begin ShowScope; end; -function TDebugEvaluatorVisitor.GetVisitorFactory: TVisitorFactory; +function TDebugEvaluatorVisitor.CreateVisitorFactory: TVisitorFactory; begin - // Return a closure that creates a new Debug visitor, - // using the context from the currently active session stack. + // Return a closure that creates a new Debug visitor, using the current context + + var callingVisitor := Self as IAstVisitor; Result := function(const AScope: IExecutionScope): IAstVisitor begin - if (not Assigned(TDebugSession.Current)) or (TDebugSession.Current.VisitorStack.Count = 0) then - raise EInvalidOpException.Create('Visitor stack is empty during function invocation.'); - - var callingVisitor := TDebugSession.Current.VisitorStack.Peek as TDebugEvaluatorVisitor; - Result := TDebugEvaluatorVisitor.Create(AScope, callingVisitor.FLog, callingVisitor.FShowScope, callingVisitor.FIndentLevel); + var visitor := callingVisitor as TDebugEvaluatorVisitor; + Result := TDebugEvaluatorVisitor.Create(AScope, visitor.FLog, visitor.FShowScope, visitor.FIndentLevel); end; end; function TDebugEvaluatorVisitor.VisitFunctionCall(const Node: IFunctionCallNode): TDataValue; begin - Assert(Assigned(TDebugSession.Current), 'No active debug session.'); - TDebugSession.Current.VisitorStack.Push(Self); + AppendLine('FunctionCall{'); + Indent; try - AppendLine('FunctionCall{'); - Indent; - try - ShowScope; - Result := inherited VisitFunctionCall(Node); - finally - Unindent; - end; - AppendLine(Format('} -> %s', [Result.ToString])); + ShowScope; + Result := inherited VisitFunctionCall(Node); finally - TDebugSession.Current.VisitorStack.Pop; + Unindent; end; + AppendLine(Format('} -> %s', [Result.ToString])); end; -// ... The rest of the TDebugEvaluatorVisitor logging methods are unchanged ... procedure TDebugEvaluatorVisitor.Indent; begin inc(FIndentLevel); @@ -170,12 +115,20 @@ var begin pad := ''; for i := 0 to FIndentLevel - 1 do - begin pad := pad + ':' + ''.PadLeft(3); - end; FLog.Add(pad + S); end; +class function TDebugEvaluatorVisitor.CreateVisitor( + const AScope: IExecutionScope; + ALog: TStrings; + AShowScope: Boolean; + AInitialIndent: Integer = 0 +): IAstVisitor; +begin + Result := TDebugEvaluatorVisitor.Create(AScope, ALog, AShowScope, AInitialIndent); +end; + procedure TDebugEvaluatorVisitor.ShowScope; var scopeDump: TArray; diff --git a/Src/AST/Myc.Ast.Evaluator.pas b/Src/AST/Myc.Ast.Evaluator.pas index 111a1ce..27256e8 100644 --- a/Src/AST/Myc.Ast.Evaluator.pas +++ b/Src/AST/Myc.Ast.Evaluator.pas @@ -22,8 +22,9 @@ type FScope: IExecutionScope; protected function IsTruthy(const AValue: TDataValue): Boolean; + // Returns a closure that can create the correct type of visitor for a lambda's body. - function GetVisitorFactory: TVisitorFactory; virtual; + function CreateVisitorFactory: TVisitorFactory; virtual; // Creates the callable closure for a lambda expression. No longer virtual. function CreateLambdaClosure( @@ -103,7 +104,7 @@ begin FScope := AScope; end; -function TEvaluatorVisitor.GetVisitorFactory: TVisitorFactory; +function TEvaluatorVisitor.CreateVisitorFactory: TVisitorFactory; begin // The production visitor returns a factory that creates another production visitor. Result := function(const AScope: IExecutionScope): IAstVisitor begin Result := TEvaluatorVisitor.Create(AScope); end; @@ -137,7 +138,7 @@ var visitorFactory: TVisitorFactory; begin // Get the appropriate factory via virtual dispatch. - visitorFactory := GetVisitorFactory(); + visitorFactory := CreateVisitorFactory(); closureValue := TDataValue.TFunc( @@ -184,6 +185,8 @@ var i: Integer; sourceAddresses: TArray; closureScope: IExecutionScope; + closureValue: TDataValue; + visitorFactory: TVisitorFactory; begin sourceAddresses := Node.Upvalues; SetLength(capturedCells, Length(sourceAddresses)); @@ -195,7 +198,48 @@ begin else closureScope := nil; - Result := CreateLambdaClosure(Node, capturedCells, closureScope); + // Get the appropriate factory via virtual dispatch. + visitorFactory := CreateVisitorFactory(); + + var scopeDescriptor := Node.ScopeDescriptor; + var params := Node.Parameters; + + closureValue := + TDataValue.TFunc( + function(const ArgValues: TArray): TDataValue + var + lambdaScope: IExecutionScope; + bodyVisitor: IAstVisitor; + i: Integer; + adr: TResolvedAddress; + begin + if (Length(ArgValues) <> Length(params)) then + raise EArgumentException.CreateFmt('Argument count mismatch: expected %d, got %d', [Length(params), Length(ArgValues)]); + + lambdaScope := TExecutionScope.Create(closureScope, scopeDescriptor, capturedCells); + + adr.Kind := akLocalOrParent; + adr.ScopeDepth := 0; + + // Set 'Self' (slot 0) to the captured closureValue for recursion. + adr.SlotIndex := 0; + lambdaScope.Cell[adr].Value := closureValue; + + // Populate the actual parameters. + for i := 0 to High(ArgValues) do + begin + adr.SlotIndex := params[i].Address.SlotIndex; + lambdaScope.Cell[adr].Value := ArgValues[i]; + end; + + // Use the injected factory to create the visitor for the lambda's body. + bodyVisitor := visitorFactory(lambdaScope); + + Result := Node.Body.Accept(bodyVisitor); + end + ); + + Result := closureValue; end; function TEvaluatorVisitor.VisitFunctionCall(const Node: IFunctionCallNode): TDataValue;