diff --git a/ASTPlayground/ASTPlayground.dproj b/ASTPlayground/ASTPlayground.dproj index 001d1a5..675c478 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.fmx b/ASTPlayground/MainForm.fmx index a01f850..f5665ba 100644 --- a/ASTPlayground/MainForm.fmx +++ b/ASTPlayground/MainForm.fmx @@ -33,7 +33,7 @@ object Form1: TForm1 end object PrettyPrintButton: TButton Position.X = 24.000000000000000000 - Position.Y = 200.000000000000000000 + Position.Y = 381.000000000000000000 TabOrder = 3 Text = 'Print' TextSettings.Trimming = None @@ -41,7 +41,7 @@ object Form1: TForm1 end object DebugButton: TButton Position.X = 24.000000000000000000 - Position.Y = 230.000000000000000000 + Position.Y = 411.000000000000000000 TabOrder = 4 Text = 'Debug' TextSettings.Trimming = None @@ -59,8 +59,8 @@ object Form1: TForm1 OnClick = RecursionButtonClick end object ShowScopeBox: TCheckBox - Position.X = 32.000000000000000000 - Position.Y = 256.000000000000000000 + Position.X = 24.000000000000000000 + Position.Y = 441.000000000000000000 TabOrder = 6 Text = 'Scope' end @@ -74,7 +74,7 @@ object Form1: TForm1 end object CrerateTriggerExampleButton: TButton Position.X = 24.000000000000000000 - Position.Y = 320.000000000000000000 + Position.Y = 168.000000000000000000 TabOrder = 9 Text = 'TriggerTest' TextSettings.Trimming = None @@ -82,7 +82,7 @@ object Form1: TForm1 end object DoTriggerButton: TButton Position.X = 24.000000000000000000 - Position.Y = 350.000000000000000000 + Position.Y = 198.000000000000000000 TabOrder = 10 Text = 'Trigger!' TextSettings.Trimming = None @@ -97,7 +97,7 @@ object Form1: TForm1 end object ClearButton: TButton Position.X = 24.000000000000000000 - Position.Y = 432.000000000000000000 + Position.Y = 472.000000000000000000 Size.Width = 80.000000000000000000 Size.Height = 22.000000000000000000 Size.PlatformDefault = False @@ -115,7 +115,7 @@ object Form1: TForm1 end object SeriesTestButton: TButton Position.X = 24.000000000000000000 - Position.Y = 152.000000000000000000 + Position.Y = 274.000000000000000000 Size.Width = 80.000000000000000000 Size.Height = 22.000000000000000000 Size.PlatformDefault = False @@ -124,6 +124,14 @@ object Form1: TForm1 TextSettings.Trimming = None OnClick = SeriesTestButtonClick end + object OHLCButton: TButton + Position.X = 24.000000000000000000 + Position.Y = 304.000000000000000000 + TabOrder = 15 + Text = 'OHLC' + TextSettings.Trimming = None + OnClick = OHLCButtonClick + end end object Panel2: TPanel Align = Client diff --git a/ASTPlayground/MainForm.pas b/ASTPlayground/MainForm.pas index 2bc5648..21e1a76 100644 --- a/ASTPlayground/MainForm.pas +++ b/ASTPlayground/MainForm.pas @@ -10,6 +10,7 @@ uses System.Classes, System.Variants, System.Generics.Collections, + System.Math, FMX.Types, FMX.Controls, FMX.Forms, @@ -31,6 +32,16 @@ uses FMX.Objects; // Added for TExecutionScope 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; @@ -48,6 +59,7 @@ type ClearButton: TButton; FlowOnlyBox: TCheckBox; SeriesTestButton: TButton; + OHLCButton: TButton; procedure ClearButtonClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure CrerateTriggerExampleButtonClick(Sender: TObject); @@ -55,6 +67,7 @@ type procedure DoTrigger2ButtonClick(Sender: TObject); procedure DoTriggerButtonClick(Sender: TObject); procedure FibonacciButtonClick(Sender: TObject); + procedure OHLCButtonClick(Sender: TObject); procedure PrettyPrintButtonClick(Sender: TObject); procedure RecursionButtonClick(Sender: TObject); procedure SeriesTestButtonClick(Sender: TObject); @@ -62,7 +75,7 @@ type procedure Test2ButtonClick(Sender: TObject); private // Stores the last AST generated by Test1 or Test2 button clicks. - FLastAst: IAstNode; + FLastAst: IExpressionNode; FGScope: IExecutionScope; FWorkspace: TAuraWorkspace; procedure WorkspaceMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single); @@ -308,12 +321,11 @@ begin Memo1.Lines.Add('--- Recursive factorial(20) ---'); sw.Start; - { + root := TAst.Block( [ - TAst.VarDecl( - TAst.Identifier('factorial'), + TAst.FunctionCall( TAst.LambdaExpr( [TAst.Identifier('n')], TAst.Block( @@ -327,7 +339,7 @@ begin TAst.Identifier('n'), boMultiply, TAst.FunctionCall( - TAst.Identifier('factorial'), + TAst.Identifier('Self'), [TAst.BinaryExpr(TAst.Identifier('n'), boSubtract, TAst.Constant(TScalar.FromInt64(1)))] ) ) @@ -335,15 +347,9 @@ begin ) ] ) - ) + ), + [TAst.Constant(TScalar.FromInt64(20))] ), - TAst.FunctionCall(TAst.Identifier('factorial'), [TAst.Constant(TScalar.FromInt64(20))]) - ] - ); -} - root := - TAst.Block( - [ TAst.VarDecl( TAst.Identifier('factorial'), TAst.LambdaExpr( @@ -357,7 +363,7 @@ begin TAst.Identifier('n'), boMultiply, TAst.FunctionCall( - TAst.Identifier('factorial'), + TAst.Identifier('Self'), [TAst.BinaryExpr(TAst.Identifier('n'), boSubtract, TAst.Constant(TScalar.FromInt64(1)))] ) ) @@ -383,16 +389,6 @@ begin end; procedure TForm1.SeriesTestButtonClick(Sender: TObject); -type - // A test record to generate a definition from. - TOHLCV = record - Timestamp: TDateTime; - Open: Double; - High: Double; - Low: Double; - Close: Double; - Volume: Int64; - end; var jsonDef: string; recordDef: TScalarRecordDefinition; @@ -457,8 +453,7 @@ begin TAst.VarDecl( TAst.Identifier('secondClose'), TAst.Indexer(TAst.Identifier('closeColumn'), TAst.Constant(TScalar.FromInt64(1))) - ), - TAst.AssignResult(TAst.Identifier('secondClose')) + ) ] ) ); @@ -527,7 +522,7 @@ begin TAst.Identifier('b'), TAst.BinaryExpr(TAst.Identifier('a'), boMultiply, TAst.Constant(TScalar.FromInt64(2))) ), - TAst.AssignResult(TAst.BinaryExpr(TAst.Identifier('a'), boAdd, TAst.Identifier('b'))) + TAst.BinaryExpr(TAst.Identifier('a'), boAdd, TAst.Identifier('b')) ] ) ); @@ -570,14 +565,11 @@ begin TAst.Identifier('createStrategyInstance'), TAst.LambdaExpr( [TAst.Identifier('offset')], - Tast.VarDecl( - TAst.Identifier('Result'), - TAst.Block( - [ - TAst.VarDecl(TAst.Identifier('baseValue'), TAst.Constant(TScalar.FromInt64(100))), - TAst.BinaryExpr(TAst.Identifier('baseValue'), boAdd, TAst.Identifier('offset')) - ] - ) + TAst.Block( + [ + TAst.VarDecl(TAst.Identifier('baseValue'), TAst.Constant(TScalar.FromInt64(100))), + TAst.BinaryExpr(TAst.Identifier('baseValue'), boAdd, TAst.Identifier('offset')) + ] ) ) ), @@ -721,4 +713,183 @@ begin end; end; +procedure TForm1.OHLCButtonClick(Sender: TObject); +const + numRecs = 1000; + lookback = 100; + smaSlowLength = 20; + smaFastLength = 5; +var + recordDef: TScalarRecordDefinition; + series: TScalarRecordSeries; + setupAst: IExpressionNode; + callAst: IExpressionNode; + visitor: IAstVisitor; + i: Integer; + lastClose: Double; + ohlcvRec: TOHLCV; + recordValue: TScalarRecord; + resultValue: TAstValue; + sw: TStopwatch; +begin + // 1. Setup + Memo1.Lines.Clear; + Memo1.Lines.Add(Format('--- Simulating SMA Crossover Strategy for %d ticks ---', [numRecs])); + FGScope.Clear; + RegisterNativeFunctions(FGScope); + sw := TStopwatch.StartNew; + + recordDef := TRttiAstHelper.JsonToRecordDefinition(TRttiAstHelper.RecordDefinitionToJson(TypeInfo(TOHLCV))); + series := TScalarRecordSeries.Create(recordDef); + + // 2. Create the setup AST. This block defines all necessary functions. + setupAst := + TAst.Block( + [ + // let sumRange = (s, n) => (n <= 0) ? 0.0 : (s[n - 1] + sumRange(s, n - 1)); + TAst.VarDecl( + TAst.Identifier('sumRange'), + TAst.LambdaExpr( + [TAst.Identifier('s'), TAst.Identifier('n')], + TAst.TernaryExpr( + TAst.BinaryExpr(TAst.Identifier('n'), boLessOrEqual, TAst.Constant(TScalar.FromInt64(0))), + TAst.Constant(TScalar.FromDouble(0.0)), // Base case for recursion + TAst.BinaryExpr( + TAst.Indexer( + TAst.Identifier('s'), + TAst.BinaryExpr(TAst.Identifier('n'), boSubtract, TAst.Constant(TScalar.FromInt64(1))) + ), + boAdd, + TAst.FunctionCall( + TAst.Identifier('sumRange'), + [ + TAst.Identifier('s'), + TAst.BinaryExpr(TAst.Identifier('n'), boSubtract, TAst.Constant(TScalar.FromInt64(1))) + ] + ) + ) + ) + ) + ), + // let CreateSMA = (len) => (series) => sumRange(series, len) / len; + TAst.VarDecl( + TAst.Identifier('CreateSMA'), + TAst.LambdaExpr( + [TAst.Identifier('len')], + TAst.LambdaExpr( + [TAst.Identifier('series')], + TAst.BinaryExpr( + TAst.FunctionCall(TAst.Identifier('sumRange'), [TAst.Identifier('series'), TAst.Identifier('len')]), + boDivide, + TAst.Identifier('len') + ) + ) + ) + ), + // let smaFast = CreateSMA(5); + TAst.VarDecl( + TAst.Identifier('smaFast'), + TAst.FunctionCall(TAst.Identifier('CreateSMA'), [TAst.Constant(TScalar.FromInt64(smaFastLength))]) + ), + // let smaSlow = CreateSMA(20); + TAst.VarDecl( + TAst.Identifier('smaSlow'), + TAst.FunctionCall(TAst.Identifier('CreateSMA'), [TAst.Constant(TScalar.FromInt64(smaSlowLength))]) + ), + // Define the main strategy function + TAst.VarDecl( + TAst.Identifier('maCrossStrategy'), + TAst.LambdaExpr( + [TAst.Identifier('ohlcv')], + TAst.Block( + [ + TAst.VarDecl( + TAst.Identifier('close'), + TAst.MemberAccess(TAst.Identifier('ohlcv'), TAst.Identifier('Close')) + ), + TAst.VarDecl( + TAst.Identifier('valSmaFast'), + TAst.FunctionCall(TAst.Identifier('smaFast'), [TAst.Identifier('close')]) + ), + TAst.VarDecl( + TAst.Identifier('valSmaSlow'), + TAst.FunctionCall(TAst.Identifier('smaSlow'), [TAst.Identifier('close')]) + ), + TAst.TernaryExpr( + TAst.BinaryExpr(TAst.Identifier('valSmaFast'), boGreater, TAst.Identifier('valSmaSlow')), + TAst.Constant(TScalar.FromInt64(1)), // Buy signal + TAst.Constant(TScalar.FromInt64(-1)) + ) // Sell signal + ] + ) + ) + ) + ] + ); + + FLastAst := setupAst; // Store for visualization + visitor := TEvaluatorVisitor.Create(FGScope); + + // Execute the setup script once to define all functions in the scope. + setupAst.Accept(visitor); + + // Create the AST for the function call that will be executed on each tick. + callAst := TAst.FunctionCall(TAst.Identifier('maCrossStrategy'), [TAst.Identifier('current_series')]); + + // 3. Simulation Loop + Memo1.Lines.Add('Starting simulation...'); + Application.ProcessMessages; + + lastClose := 1000.0; + Randomize; + + var nw := Now; + for i := 1 to numRecs do + begin + // Generate a random OHLCV record + 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; + + recordValue := + TScalarRecord.Create( + recordDef, + [ + 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.Add(recordValue, lookback); + + // Execute the strategy if we have enough data for the slowest indicator + if series.TotalCount >= smaSlowLength then + begin + // Place the current data into the scope for the callAst to find + FGScope.SetValue('current_series', TAstValue.FromRecordSeries(series)); + + // Correctly call the strategy function via its identifier + resultValue := callAst.Accept(visitor); + + if (i mod 100 = 0) or (i = numRecs) 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; + end. diff --git a/ASTPlayground/Myc.Ast.Visualizer.pas b/ASTPlayground/Myc.Ast.Visualizer.pas index 6520d9e..338a571 100644 --- a/ASTPlayground/Myc.Ast.Visualizer.pas +++ b/ASTPlayground/Myc.Ast.Visualizer.pas @@ -76,6 +76,7 @@ type const Title, Details: string; const InPin, OutPin: String; IsExec: Boolean; + HasResult: Boolean; const ChildVisitorProc: TChildVisitorProc ): TAuraNode; function VisitOperatorNode( @@ -181,7 +182,8 @@ implementation uses System.Math, - System.StrUtils; + System.StrUtils, + Myc.Data.Scalar; type // This visitor converts an AST expression subtree into a single string. @@ -284,7 +286,7 @@ var begin sb := TStringBuilder.Create; try - sb.Append('('); + sb.Append(#$03BB + '('); if Length(Node.Parameters) > 0 then begin for i := 0 to High(Node.Parameters) do @@ -677,6 +679,7 @@ begin '', '', true, + false, procedure(const Visitor: IAstVisitor) var expression: IExpressionNode; @@ -715,6 +718,7 @@ function TAstToAuraNodeVisitor.VisitContainerNode( const Title, Details: string; const InPin, OutPin: String; IsExec: Boolean; + HasResult: Boolean; const ChildVisitorProc: TChildVisitorProc ): TAuraNode; var @@ -724,6 +728,7 @@ var maxRight: Single; maxBottom: Single; control: TControl; + childLastResult: TAuraNodeResult; begin const pinNodeHeight = cPinSize + cVerticalPadding; @@ -764,6 +769,9 @@ begin childVisitor := TAstToAuraNodeVisitor.Create(FWorkspace, containerNode, childStartPos, FConnections, FCurrentExec.ToArray, FMode); ChildVisitorProc(childVisitor); + // Capture the result from the child visitor. + childLastResult := (childVisitor as TAstToAuraNodeVisitor).FLastResult; + FCurrentExec.Clear; FCurrentExec.AddRange((childVisitor as TAstToAuraNodeVisitor).FCurrentExec); @@ -781,7 +789,7 @@ begin // The width must be at least the title width, and large enough for children. containerNode.Width := Max(containerNode.Width, maxRight + FSpacing.X); - containerNode.Height := Max(containerNode.Height, maxBottom + FSpacing.Y + IfThen(OutPin <> '', pinNodeHeight, 0)); + containerNode.Height := Max(containerNode.Height, maxBottom + FSpacing.Y); if OutPin <> '' then begin @@ -789,8 +797,19 @@ begin exitNode.Parent := containerNode; exitNode.Height := pinNodeHeight; var exitPin := CreateEntry(exitNode); + + // Create and connect a data pin if the container returns a value. + if HasResult and Assigned(childLastResult.OutputPin) then + begin + var dataResultPin := CreateInput(exitNode, 'Result'); + FConnections.Add(TPinConnection.Create(childLastResult.OutputPin, dataResultPin)); + end; + FinalizeNodeLayout(exitNode); + // Enlarge for the exit node + containerNode.Height := Max(containerNode.Height, maxBottom + FSpacing.Y + exitNode.Height); + exitNode.Position.Point := TPointF.Create(containerNode.Width - exitNode.Width, containerNode.Height - exitNode.Height); if not IsExec then @@ -1022,11 +1041,12 @@ begin // Use the helper to create and populate the container node. lambdaNode := VisitContainerNode( - 'Lambda', + #$03BB, paramStr, 'call', 'return', false, + true, procedure(const Visitor: IAstVisitor) begin Node.Body.Accept(Visitor); end ); diff --git a/Src/AST/Myc.Ast.Evaluator.pas b/Src/AST/Myc.Ast.Evaluator.pas index f065cd5..4b1f258 100644 --- a/Src/AST/Myc.Ast.Evaluator.pas +++ b/Src/AST/Myc.Ast.Evaluator.pas @@ -303,16 +303,8 @@ begin memberName := Node.Member.Name; case baseValue.Kind of - avkRecordSeries: - begin - var series := baseValue.AsRecordSeries; - Result := TAstValue.FromMemberSeries(series.CreateMemberSeries(memberName)); - end; - avkRecord: - begin - var recordValue := baseValue.AsRecord; - Result := TAstValue.FromScalar(recordValue.Items[memberName]); - end; + avkRecordSeries: Result := TAstValue.FromMemberSeries(baseValue.AsRecordSeries.CreateMemberSeries(memberName)); + avkRecord: Result := TAstValue.FromScalar(baseValue.AsRecord.Items[memberName]); else raise EArgumentException.Create('Member access operator `.` is not supported for this value type.'); end; @@ -381,6 +373,8 @@ begin end; innerVisitor := Self.CreateVisitorForScope(callScope); + + callScope.SetValue('Self', calleeValue); callScope.SetValue('Result', TAstValue.Void); Result := closure.Body.Accept(innerVisitor); end @@ -392,56 +386,78 @@ function TEvaluatorVisitor.VisitBinaryExpression(const Node: IBinaryExpressionNo var leftValue, rightValue: TAstValue; leftScalar, rightScalar: TScalar; - leftVal, rightVal: Int64; - boolResult: Boolean; - intResult: Int64; begin + // Implemented binary operators for Int64 and Double. leftValue := Node.Left.Accept(Self); rightValue := Node.Right.Accept(Self); - if leftValue.Kind <> rightValue.Kind then - raise ENotSupportedException.Create('Binary operations are only supported for compatible types.'); - - if leftValue.Kind <> avkScalar then - begin + if (leftValue.Kind <> avkScalar) or (rightValue.Kind <> avkScalar) then raise ENotSupportedException.Create('Binary operations are only supported for scalar types.'); - end; leftScalar := leftValue.AsScalar; rightScalar := rightValue.AsScalar; - if (leftScalar.Kind <> skInt64) or (rightScalar.Kind <> skInt64) then + if (leftScalar.Kind <> rightScalar.Kind) then begin - raise ENotSupportedException.Create('Binary operations currently only support Int64.'); - end; - - leftVal := leftScalar.Value.AsInt64; - rightVal := rightScalar.Value.AsInt64; - - case Node.Operator of - boEqual: boolResult := (leftVal = rightVal); - boNotEqual: boolResult := (leftVal <> rightVal); - boLess: boolResult := (leftVal < rightVal); - boGreater: boolResult := (leftVal > rightVal); - boLessOrEqual: boolResult := (leftVal <= rightVal); - boGreaterOrEqual: boolResult := (leftVal >= rightVal); - else - case Node.Operator of - boAdd: intResult := leftVal + rightVal; - boSubtract: intResult := leftVal - rightVal; - boMultiply: intResult := leftVal * rightVal; - boDivide: intResult := leftVal div rightVal; + // Automatic type promotion from Int64 to Double. + if (leftScalar.Kind = skInt64) and (rightScalar.Kind = skDouble) then + begin + leftScalar := TScalar.FromDouble(leftScalar.Value.AsInt64); + end + else if (leftScalar.Kind = skDouble) and (rightScalar.Kind = skInt64) then + begin + rightScalar := TScalar.FromDouble(rightScalar.Value.AsInt64); + end else - raise ENotSupportedException.Create('Operator not supported.'); + begin + // If types are still different after promotion, they are incompatible. + raise ENotSupportedException.Create( + 'Binary operations are only supported for compatible types. ' + leftScalar.ToString + ' ' + rightScalar.ToString); end; - Result := TAstValue.FromScalar(TScalar.FromInt64(intResult)); - exit; end; - if boolResult then - Result := TAstValue.FromScalar(TScalar.FromBoolean(true)) + case leftScalar.Kind of + skInt64: + begin + var leftVal := leftScalar.Value.AsInt64; + var rightVal := rightScalar.Value.AsInt64; + case Node.Operator of + boAdd: Result := TAstValue.FromScalar(TScalar.FromInt64(leftVal + rightVal)); + boSubtract: Result := TAstValue.FromScalar(TScalar.FromInt64(leftVal - rightVal)); + boMultiply: Result := TAstValue.FromScalar(TScalar.FromInt64(leftVal * rightVal)); + boDivide: Result := TAstValue.FromScalar(TScalar.FromInt64(leftVal div rightVal)); + boEqual: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal = rightVal)); + boNotEqual: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal <> rightVal)); + boLess: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal < rightVal)); + boGreater: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal > rightVal)); + boLessOrEqual: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal <= rightVal)); + boGreaterOrEqual: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal >= rightVal)); + else + raise ENotSupportedException.Create('Operator not supported for Int64.'); + end; + end; + skDouble: + begin + var leftVal := leftScalar.Value.AsDouble; + var rightVal := rightScalar.Value.AsDouble; + case Node.Operator of + boAdd: Result := TAstValue.FromScalar(TScalar.FromDouble(leftVal + rightVal)); + boSubtract: Result := TAstValue.FromScalar(TScalar.FromDouble(leftVal - rightVal)); + boMultiply: Result := TAstValue.FromScalar(TScalar.FromDouble(leftVal * rightVal)); + boDivide: Result := TAstValue.FromScalar(TScalar.FromDouble(leftVal / rightVal)); + boEqual: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal = rightVal)); + boNotEqual: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal <> rightVal)); + boLess: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal < rightVal)); + boGreater: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal > rightVal)); + boLessOrEqual: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal <= rightVal)); + boGreaterOrEqual: Result := TAstValue.FromScalar(TScalar.FromBoolean(leftVal >= rightVal)); + else + raise ENotSupportedException.Create('Operator not supported for Double.'); + end; + end; else - Result := TAstValue.FromScalar(TScalar.FromBoolean(false)); + raise ENotSupportedException.Create('Binary operations are not supported for this scalar type.'); + end; end; function TEvaluatorVisitor.VisitUnaryExpression(const Node: IUnaryExpressionNode): TAstValue; @@ -449,21 +465,27 @@ var rightValue: TAstValue; rightScalar: TScalar; begin + // Implemented unary operators for Int64, Double, and Boolean. rightValue := Node.Right.Accept(Self); - if rightValue.Kind <> avkScalar then - raise ENotSupportedException.Create('Unary operations are only supported for scalar types.'); - - rightScalar := rightValue.AsScalar; case Node.Operator of uoNegate: begin - if (rightScalar.Kind = skInt64) then - Result := TAstValue.FromScalar(TScalar.FromInt64(-rightScalar.Value.AsInt64)) + if (rightValue.Kind <> avkScalar) then + raise ENotSupportedException.Create('Unary "-" is only supported for scalar types.'); + rightScalar := rightValue.AsScalar; + case rightScalar.Kind of + skInt64: Result := TAstValue.FromScalar(TScalar.FromInt64(-rightScalar.Value.AsInt64)); + skDouble: Result := TAstValue.FromScalar(TScalar.FromDouble(-rightScalar.Value.AsDouble)); else - raise ENotSupportedException.Create('Unary "-" not supported for this scalar type.'); + raise ENotSupportedException.Create('Unary "-" is not supported for this scalar type.'); + end; + end; + uoNot: + begin + // "not" operates on the truthiness of the value. + Result := TAstValue.FromScalar(TScalar.FromBoolean(not IsTruthy(rightValue))); end; - uoNot: raise ENotImplemented.Create('Unary "not" operator is not yet implemented'); else raise ENotSupportedException.Create('Unary operator not supported'); end; diff --git a/Src/AST/Myc.Ast.Nodes.pas b/Src/AST/Myc.Ast.Nodes.pas index e7f5028..6f5d7a9 100644 --- a/Src/AST/Myc.Ast.Nodes.pas +++ b/Src/AST/Myc.Ast.Nodes.pas @@ -82,19 +82,19 @@ type function GetIsVoid: Boolean; inline; public class operator Initialize(out Dest: TAstValue); - class function Void: TAstValue; static; - class function FromScalar(const AValue: TScalar): TAstValue; static; - class function FromClosure(const AValue: IEvaluatorClosure): TAstValue; static; - class function FromText(const AValue: String): TAstValue; static; - class function FromRecordSeries(const AValue: TScalarRecordSeries): TAstValue; static; - class function FromRecord(const AValue: TScalarRecord): TAstValue; static; - class function FromMemberSeries(const AValue: TScalarMemberSeries): TAstValue; static; - function AsScalar: TScalar; - function AsClosure: IEvaluatorClosure; - function AsText: String; - function AsRecordSeries: TScalarRecordSeries; - function AsRecord: TScalarRecord; - function AsMemberSeries: TScalarMemberSeries; + class function Void: TAstValue; inline; static; + class function FromScalar(const AValue: TScalar): TAstValue; inline; static; + class function FromClosure(const AValue: IEvaluatorClosure): TAstValue; inline; static; + class function FromText(const AValue: String): TAstValue; inline; static; + class function FromRecordSeries(const AValue: TScalarRecordSeries): TAstValue; inline; static; + class function FromRecord(const AValue: TScalarRecord): TAstValue; inline; static; + class function FromMemberSeries(const AValue: TScalarMemberSeries): TAstValue; inline; static; + function AsScalar: TScalar; inline; + function AsClosure: IEvaluatorClosure; inline; + function AsText: String; inline; + function AsRecordSeries: TScalarRecordSeries; inline; + function AsRecord: TScalarRecord; inline; + function AsMemberSeries: TScalarMemberSeries; inline; function ToString: String; property IsVoid: Boolean read GetIsVoid; property Kind: TAstValueKind read GetKind; diff --git a/Src/AST/Myc.Ast.pas b/Src/AST/Myc.Ast.pas index 2647937..4ad7901 100644 --- a/Src/AST/Myc.Ast.pas +++ b/Src/AST/Myc.Ast.pas @@ -33,7 +33,6 @@ type class function Assign(const AIdentifier: IIdentifierNode; const AValue: IExpressionNode): IAssignmentNode; static; class function AssignResult(const AValue: IExpressionNode): IAssignmentNode; static; class function Indexer(const ABase: IExpressionNode; const AIndex: IExpressionNode): IIndexerNode; static; - // Added factory for the new member access node. class function MemberAccess(const ABase: IExpressionNode; const AMember: IIdentifierNode): IMemberAccessNode; static; end; diff --git a/Src/Data/Myc.Data.Scalar.JSON.pas b/Src/Data/Myc.Data.Scalar.JSON.pas index aad5714..d58acca 100644 --- a/Src/Data/Myc.Data.Scalar.JSON.pas +++ b/Src/Data/Myc.Data.Scalar.JSON.pas @@ -32,7 +32,6 @@ uses class function TRttiAstHelper.JsonToRecordDefinition(const AJson: string): TScalarRecordDefinition; var jsonValue: TJSONValue; - rootObj: TJSONObject; fieldsArray: TJSONArray; i: Integer; fieldObj: TJSONObject; @@ -44,12 +43,11 @@ begin exit; try - if not (jsonValue is TJSONObject) then + // The root element is expected to be a JSON array directly. + if not (jsonValue is TJSONArray) then exit; - rootObj := jsonValue as TJSONObject; - if not rootObj.TryGetValue('fields', fieldsArray) then - exit; + fieldsArray := jsonValue as TJSONArray; SetLength(fields, fieldsArray.Count); for i := 0 to fieldsArray.Count - 1 do @@ -75,11 +73,9 @@ var rttiType: TRttiType; rttiRecordType: TRttiRecordType; field: TRttiField; - rootObj: TJSONObject; fieldsArray: TJSONArray; fieldObj: TJSONObject; begin - // 1. Create TRttiContext and get TRttiRecordType. ctx := TRttiContext.Create; rttiType := ctx.GetType(ATypeInfo); @@ -88,32 +84,21 @@ begin rttiRecordType := rttiType as TRttiRecordType; - // 2. Create root TJSONObject and a TJSONArray for 'fields'. - rootObj := TJSONObject.Create; + // The root element is now the array itself. + fieldsArray := TJSONArray.Create; try - fieldsArray := TJSONArray.Create; - rootObj.AddPair('fields', fieldsArray); - - // 3. Loop through all fields of the record type. for field in rttiRecordType.GetFields do begin - // 4a. Create a TJSONObject for the field definition. fieldObj := TJSONObject.Create; - - // 4b. Add field name. fieldObj.AddPair('name', TJSONString.Create(field.Name)); - - // 4c. Call TypeToScalarKind to get the type and add it. fieldObj.AddPair('kind', TJSONString.Create(GetEnumName(TypeInfo(TScalarKind), Ord(TypeToScalarKind(field.FieldType))))); - - // 4d. Add the field object to the 'fields' array. fieldsArray.Add(fieldObj); end; - // 5. Convert the root JSON object to a string and set it as Result. - Result := rootObj.ToJSON; + // Convert the array directly to a JSON string. + Result := fieldsArray.ToJSON; finally - rootObj.Free; + fieldsArray.Free; end; end; diff --git a/Src/Data/Myc.Data.Scalar.pas b/Src/Data/Myc.Data.Scalar.pas index 93f9a82..4c5b5a5 100644 --- a/Src/Data/Myc.Data.Scalar.pas +++ b/Src/Data/Myc.Data.Scalar.pas @@ -161,18 +161,35 @@ type TScalarMemberSeries = record private - FDef: TScalarRecordDefinition; FArray: TChunkArray; - FElement: Integer; - function GetCount: Int64; inline; + FElementIdx: Integer; + FKind: TScalarKind; + FElemCount: Integer; + function GetCount: Int64; function GetItems(Idx: Integer): TScalar; public - constructor Create(const ADef: TScalarRecordDefinition; const AArray: TChunkArray; AElement: Integer); - function GetKind: TScalarKind; inline; + constructor Create(AKind: TScalarKind; const AArray: TChunkArray; AElementIdx, AElemCount: Integer); property Count: Int64 read GetCount; - property Element: Integer read FElement; property Items[Idx: Integer]: TScalar read GetItems; default; - property Kind: TScalarKind read GetKind; + property Kind: TScalarKind read FKind; + end; + + // A time series of scalar tuples, optimized for memory and access speed. + TScalarTupleSeries = record + private + FDef: TArray; + FArray: TChunkArray; + FTotalCount: Int64; + function GetCount: Int64; + function GetItems(Idx: Integer): TScalarTuple; + public + constructor Create(const ADef: TArray); + procedure Add(const Item: TScalarTuple; Lookback: Int64 = -1); + function CreateMemberSeries(Idx: Integer): TScalarMemberSeries; + property Count: Int64 read GetCount; + property Def: TArray read FDef; + property Items[Idx: Integer]: TScalarTuple read GetItems; default; + property TotalCount: Int64 read FTotalCount; end; // A time series of scalar records, optimized for memory and access speed. @@ -181,12 +198,15 @@ type FDef: TScalarRecordDefinition; FArray: TChunkArray; FTotalCount: Int64; + function GetCount: Int64; function GetDef: TScalarRecordDefinition; inline; function GetItems(Idx: Integer): TScalarRecord; public constructor Create(const ADef: TScalarRecordDefinition); procedure Add(const Item: TScalarRecord; Lookback: Int64 = -1); + function AsTupleSeries: TScalarTupleSeries; function CreateMemberSeries(const Field: String): TScalarMemberSeries; + property Count: Int64 read GetCount; property Def: TScalarRecordDefinition read GetDef; property Items[Idx: Integer]: TScalarRecord read GetItems; default; property TotalCount: Int64 read FTotalCount; @@ -452,74 +472,109 @@ begin Result := FItems; end; -{ TScalarRecordSeries } +{ TScalarTupleSeries } -// Implements a time series of scalar records using a chunk array for efficient storage. -// Each record's fields are stored sequentially in the TChunkArray. - -constructor TScalarRecordSeries.Create(const ADef: TScalarRecordDefinition); +constructor TScalarTupleSeries.Create(const ADef: TArray); begin - Assert(Length(ADef.Fields) > 0); + // Implements a time series of scalar tuples using a chunk array for efficient storage. + Assert(Length(ADef) > 0, 'Tuple definition cannot be empty.'); FDef := ADef; - FArray := Default(TChunkArray); FTotalCount := 0; end; -procedure TScalarRecordSeries.Add(const Item: TScalarRecord; Lookback: Int64 = -1); +procedure TScalarTupleSeries.Add(const Item: TScalarTuple; Lookback: Int64 = -1); +var + values: TArray; + i: Integer; + tupleSize: Integer; + maxCount: Integer; begin - FArray.Add(Item.Fields, Length(FDef.Fields) * Lookback); + tupleSize := Length(FDef); + Assert(Length(Item) = tupleSize, 'Tuple does not match series definition.'); + + // Extract TScalarValue from TScalarTuple. + SetLength(values, tupleSize); + for i := 0 to tupleSize - 1 do + begin + Assert(Item[i].Kind = FDef[i], 'Tuple element kind mismatch.'); + values[i] := Item[i].Value; + end; + + if Lookback < 0 then + maxCount := -1 + else + maxCount := tupleSize * Lookback; + + FArray.Add(values, maxCount); inc(FTotalCount); end; -function TScalarRecordSeries.CreateMemberSeries(const Field: String): TScalarMemberSeries; +function TScalarTupleSeries.CreateMemberSeries(Idx: Integer): TScalarMemberSeries; +var + tupleSize: Integer; begin - var elem := FDef.IndexOf(Field); - if elem < 0 then - raise EArgumentException.CreateFmt('Field "%s" not found in record definition.', [Field]); + tupleSize := Length(FDef); + if (Idx < 0) or (Idx >= tupleSize) then + raise EArgumentException.CreateFmt('Index %d is out of bounds for a tuple of size %d.', [Idx, tupleSize]); - Result := TScalarMemberSeries.Create(FDef, FArray, elem); + // Creates a series view for a specific member of the tuple. + Result := TScalarMemberSeries.Create(FDef[Idx], FArray, Idx, tupleSize); end; -function TScalarRecordSeries.GetDef: TScalarRecordDefinition; +function TScalarTupleSeries.GetCount: Int64; begin - Result := FDef; + if Length(FDef) = 0 then + exit(0); + Result := FArray.Count div Length(FDef); end; -function TScalarRecordSeries.GetItems(Idx: Integer): TScalarRecord; +function TScalarTupleSeries.GetItems(Idx: Integer): TScalarTuple; var values: TArray; - fieldCount: Integer; + tupleSize, i: Integer; begin - fieldCount := Length(FDef.Fields); - SetLength(values, fieldCount); - Move(FArray.ItemRef[(FArray.Count - fieldCount) - (Idx * fieldCount)]^, values[0], sizeof(TScalarValue) * fieldCount); - Result.Create(FDef, values); + tupleSize := Length(FDef); + Assert(tupleSize > 0); + Assert((Idx >= 0) and (Idx < (FArray.Count div tupleSize))); + + // Create a temporary array to hold the values for one tuple. + SetLength(values, tupleSize); + // Copy the tuple data from the chunk array (latest item is at the end). + Move(FArray.ItemRef[(FArray.Count - tupleSize) - (Idx * tupleSize)]^, values[0], sizeof(TScalarValue) * tupleSize); + + // Reconstruct the TScalarTuple from the values and the definition. + SetLength(Result, tupleSize); + for i := 0 to tupleSize - 1 do + Result[i] := TScalar.Create(FDef[i], values[i]); end; -constructor TScalarMemberSeries.Create(const ADef: TScalarRecordDefinition; const AArray: TChunkArray; AElement: Integer); +{ TScalarMemberSeries } + +constructor TScalarMemberSeries.Create(AKind: TScalarKind; const AArray: TChunkArray; AElementIdx, AElemCount: Integer); begin - FDef := ADef; + Assert(AArray.Count mod AElemCount = 0); + Assert(AElementIdx >= 0); + Assert(AElementIdx < AElemCount); + + FKind := AKind; FArray := AArray; - FElement := AElement; + FElementIdx := AElementIdx; + FElemCount := AElemCount; end; function TScalarMemberSeries.GetCount: Int64; begin - Result := FArray.Count div Length(FDef.Fields); + if FElemCount = 0 then + exit(0); + Result := FArray.Count div FElemCount; end; function TScalarMemberSeries.GetItems(Idx: Integer): TScalar; -var - fieldCount: Integer; begin - fieldCount := Length(FDef.Fields); - Result.Create(FDef.Fields[FElement].Kind, FArray.Items[(FArray.Count - fieldCount) - (Idx * fieldCount) + FElement]); + Result.Create(FKind, FArray.Items[(FArray.Count - FElemCount) - (Idx * FElemCount) + FElementIdx]); end; -function TScalarMemberSeries.GetKind: TScalarKind; -begin - Result := FDef.Fields[FElement].Kind; -end; +{ TScalarRecordDefinition } constructor TScalarRecordDefinition.Create(const AFields: TArray); begin @@ -538,6 +593,72 @@ begin Result := -1; end; +{ TScalarRecordSeries } + +// Implements a time series of scalar records using a chunk array for efficient storage. +// Each record's fields are stored sequentially in the TChunkArray. + +constructor TScalarRecordSeries.Create(const ADef: TScalarRecordDefinition); +begin + Assert(Length(ADef.Fields) > 0); + FDef := ADef; + FTotalCount := 0; +end; + +procedure TScalarRecordSeries.Add(const Item: TScalarRecord; Lookback: Int64 = -1); +begin + FArray.Add(Item.Fields, Length(FDef.Fields) * Lookback); + inc(FTotalCount); +end; + +function TScalarRecordSeries.AsTupleSeries: TScalarTupleSeries; +var + tupleDef: TArray; + i: Integer; +begin + // Extract the kinds from the record definition to create a tuple definition. + SetLength(tupleDef, Length(FDef.Fields)); + for i := 0 to High(FDef.Fields) do + tupleDef[i] := FDef.Fields[i].Kind; + + // Create the tuple series with the new definition. + Result := TScalarTupleSeries.Create(tupleDef); + + // The underlying data is compatible, so we can share it. + Result.FArray := FArray; + Result.FTotalCount := FTotalCount; +end; + +function TScalarRecordSeries.CreateMemberSeries(const Field: String): TScalarMemberSeries; +begin + var elem := FDef.IndexOf(Field); + if elem < 0 then + raise EArgumentException.CreateFmt('Field "%s" not found in record definition.', [Field]); + + Result := TScalarMemberSeries.Create(FDef.Fields[elem].Kind, FArray, elem, Length(FDef.Fields)); +end; + +function TScalarRecordSeries.GetCount: Int64; +begin + Result := FArray.Count div Length(FDef.Fields); +end; + +function TScalarRecordSeries.GetDef: TScalarRecordDefinition; +begin + Result := FDef; +end; + +function TScalarRecordSeries.GetItems(Idx: Integer): TScalarRecord; +var + values: TArray; + fieldCount: Integer; +begin + fieldCount := Length(FDef.Fields); + SetLength(values, fieldCount); + Move(FArray.ItemRef[(FArray.Count - fieldCount) - (Idx * fieldCount)]^, values[0], sizeof(TScalarValue) * fieldCount); + Result.Create(FDef, values); +end; + initialization Assert(sizeof(TScalarValue) = 8);