diff --git a/Src/AST/Myc.Ast.RTL.Core.pas b/Src/AST/Myc.Ast.RTL.Core.pas index 4b0c577..17adfd2 100644 --- a/Src/AST/Myc.Ast.RTL.Core.pas +++ b/Src/AST/Myc.Ast.RTL.Core.pas @@ -3,6 +3,7 @@ unit Myc.Ast.RTL.Core; interface uses + system.sysutils, Myc.Utils, Myc.Data.Scalar, Myc.Data.Value, @@ -12,74 +13,84 @@ type // Contains the "pure" native implementations. TRtlFunctions = record public - // (* --- Dynamic Fallbacks --- *) + // (* --- Dynamic Fallbacks (Interpreter) --- *) + + // Arithmetic [TRtlExport('+')] class function Add(const Args: TArray): TDataValue; overload; static; - [TRtlExport('-')] class function Subtract(const Args: TArray): TDataValue; static; - [TRtlExport('*')] class function Multiply(const Args: TArray): TDataValue; static; - [TRtlExport('/')] class function Divide(const Args: TArray): TDataValue; static; + [TRtlExport('div')] + class function IntDivide(const Args: TArray): TDataValue; static; + [TRtlExport('mod')] + class function Modulus(const Args: TArray): TDataValue; static; + + // Comparison [TRtlExport('=')] class function Equal(const Args: TArray): TDataValue; static; - [TRtlExport('<>')] class function NotEqual(const Args: TArray): TDataValue; static; - [TRtlExport('<')] class function LessThan(const Args: TArray): TDataValue; static; - [TRtlExport('<=')] class function LessThanOrEqual(const Args: TArray): TDataValue; static; - [TRtlExport('>')] class function GreaterThan(const Args: TArray): TDataValue; static; - [TRtlExport('>=')] class function GreaterThanOrEqual(const Args: TArray): TDataValue; static; + // Logic / Bitwise [TRtlExport('not')] class function LogicalNot(const Args: TArray): TDataValue; static; + [TRtlExport('and')] + class function BitwiseAnd(const Args: TArray): TDataValue; static; + [TRtlExport('or')] + class function BitwiseOr(const Args: TArray): TDataValue; static; + [TRtlExport('xor')] + class function BitwiseXor(const Args: TArray): TDataValue; static; + [TRtlExport('shl')] + class function LeftShift(const Args: TArray): TDataValue; static; + [TRtlExport('shr')] + class function RightShift(const Args: TArray): TDataValue; static; // (* Dynamic fallbacks for TScalar input *) [TRtlExport('Abs')] class function Abs(const Arg: TScalar): TScalar; static; - + [TRtlExport('Round')] + class function Round(const Arg: TScalar): TScalar; static; // <-- Added [TRtlExport('Trunc')] class function Trunc(const Arg: TScalar): TScalar; static; - [TRtlExport('Ceil')] class function Ceil(const Arg: TScalar): TScalar; static; - [TRtlExport('Floor')] class function Floor(const Arg: TScalar): TScalar; static; - [TRtlExport('Sign')] class function Sign(const Arg: TScalar): TScalar; static; + // (* DateTime Constructors *) + [TRtlExport('Now', False)] + class function Now(const Args: TArray): TDataValue; static; + [TRtlExport('Date')] + class function Date(const Args: TArray): TDataValue; static; + // (* Other dynamic functions *) [TRtlExport('Memoize')] class function Memoize(const Args: TArray): TDataValue; static; - [TRtlExport('Map')] class function Map(const Args: TArray): TDataValue; static; - [TRtlExport('Reduce')] class function Reduce(const Args: TArray): TDataValue; static; - [TRtlExport('Where')] class function Where(const Args: TArray): TDataValue; static; - [TRtlExport('Any')] class function Any(const Args: TArray): TDataValue; static; - // (* --- Static Specializations (for Monomorphization) --- *) - // (* Schema: FunctionName_Arg1_ArgN_Return *) + // (* --- Static Specializations (Monomorphization Targets) --- *) // Add [TRtlExport('+', True)] @@ -111,7 +122,7 @@ type [TRtlExport('*', True)] class function Multiply_Float_Ordinal_Float(A: Double; B: Int64): Double; static; - // Divide (NOT pure due to DivByZero) + // Divide (Impure due to EDivByZero potential) [TRtlExport('/')] class function Divide_Ordinal_Ordinal_Float(A, B: Int64): Double; static; [TRtlExport('/')] @@ -121,7 +132,13 @@ type [TRtlExport('/')] class function Divide_Float_Ordinal_Float(A: Double; B: Int64): Double; static; - // Comparisons (Return Ordinal) + // Integer Math (Impure due to EDivByZero) + [TRtlExport('div')] + class function Div_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; static; + [TRtlExport('mod')] + class function Mod_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; static; + + // Comparisons [TRtlExport('=', True)] class function Equal_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; static; [TRtlExport('=', True)] @@ -180,6 +197,18 @@ type [TRtlExport('>=', True)] class function GreaterOrEqual_Float_Ordinal_Ordinal(A: Double; B: Int64): Int64; static; + // Bitwise Static + [TRtlExport('and', True)] + class function And_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; static; + [TRtlExport('or', True)] + class function Or_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; static; + [TRtlExport('xor', True)] + class function Xor_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; static; + [TRtlExport('shl', True)] + class function Shl_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; static; + [TRtlExport('shr', True)] + class function Shr_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; static; + // Unary [TRtlExport('-', True)] class function Negate_Ordinal_Ordinal(A: Int64): Int64; static; @@ -195,6 +224,11 @@ type [TRtlExport('Abs', True)] class function Abs_Float_Float(A: Double): Double; static; + [TRtlExport('Round', True)] + class function Round_Ordinal_Ordinal(A: Int64): Int64; static; // <-- Added + [TRtlExport('Round', True)] + class function Round_Float_Ordinal(A: Double): Int64; static; // <-- Added + [TRtlExport('Trunc', True)] class function Trunc_Ordinal_Ordinal(A: Int64): Int64; static; [TRtlExport('Trunc', True)] @@ -204,140 +238,143 @@ type implementation uses - System.SysUtils, - System.Generics.Collections, - System.Math, + system.generics.collections, + system.math, + system.dateutils, Myc.Data.Decimal; { TRtlFunctions - Operator Implementations } class function TRtlFunctions.Add(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator + requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.Add, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator +.'); - Result := res; + Result := Args[0].AsScalar + Args[1].AsScalar; end; class function TRtlFunctions.Subtract(const Args: TArray): TDataValue; -var - res: TScalar; begin - if Length(Args) = 1 then // Unary negation - begin - if not TScalar.TryUnaryOperation(TScalar.TUnaryOp.Negate, Args[0], res) then - raise EArgumentException.Create('Invalid argument for unary operator -.'); - end - else if Length(Args) = 2 then // Binary subtraction - begin - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.Subtract, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for binary operator -.'); - end + if Length(Args) = 1 then + Result := -Args[0].AsScalar + else if Length(Args) = 2 then + Result := Args[0].AsScalar - Args[1].AsScalar else raise EArgumentException.Create('Operator - requires 1 or 2 arguments.'); - Result := res; end; class function TRtlFunctions.Multiply(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator * requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.Multiply, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator *.'); - Result := res; + Result := Args[0].AsScalar * Args[1].AsScalar; end; class function TRtlFunctions.Divide(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator / requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.Divide, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator /.'); - Result := res; + // Explicit check is handled in TScalar.Divide + Result := Args[0].AsScalar / Args[1].AsScalar; +end; + +class function TRtlFunctions.IntDivide(const Args: TArray): TDataValue; +begin + if Length(Args) <> 2 then + raise EArgumentException.Create('Operator div requires 2 arguments.'); + Result := Args[0].AsScalar div Args[1].AsScalar; +end; + +class function TRtlFunctions.Modulus(const Args: TArray): TDataValue; +begin + if Length(Args) <> 2 then + raise EArgumentException.Create('Operator mod requires 2 arguments.'); + Result := Args[0].AsScalar mod Args[1].AsScalar; end; class function TRtlFunctions.Equal(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator = requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.Equal, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator =.'); - Result := res; + Result := TScalar.FromBoolean(Args[0].AsScalar = Args[1].AsScalar); end; class function TRtlFunctions.NotEqual(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator <> requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.NotEqual, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator <>.'); - Result := res; + Result := TScalar.FromBoolean(Args[0].AsScalar <> Args[1].AsScalar); end; class function TRtlFunctions.LessThan(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator < requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.Less, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator <.'); - Result := res; + Result := TScalar.FromBoolean(Args[0].AsScalar < Args[1].AsScalar); end; class function TRtlFunctions.LessThanOrEqual(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator <= requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.LessOrEqual, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator <=.'); - Result := res; + Result := TScalar.FromBoolean(Args[0].AsScalar <= Args[1].AsScalar); end; class function TRtlFunctions.GreaterThan(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator > requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.Greater, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator >.'); - Result := res; + Result := TScalar.FromBoolean(Args[0].AsScalar > Args[1].AsScalar); end; class function TRtlFunctions.GreaterThanOrEqual(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 2 then raise EArgumentException.Create('Operator >= requires 2 arguments.'); - if not TScalar.TryBinaryOperation(TScalar.TBinaryOp.GreaterOrEqual, Args[0], Args[1], res) then - raise EArgumentException.Create('Invalid arguments for operator >=.'); - Result := res; + Result := TScalar.FromBoolean(Args[0].AsScalar >= Args[1].AsScalar); end; +// --- Logic / Bitwise --- + class function TRtlFunctions.LogicalNot(const Args: TArray): TDataValue; -var - res: TScalar; begin if Length(Args) <> 1 then raise EArgumentException.Create('Operator not requires 1 argument.'); - if not TScalar.TryUnaryOperation(TScalar.TUnaryOp.Not, Args[0], res) then - raise EArgumentException.Create('Invalid argument for operator not.'); - Result := res; + Result := not Args[0].AsScalar; +end; + +class function TRtlFunctions.BitwiseAnd(const Args: TArray): TDataValue; +begin + if Length(Args) <> 2 then + raise EArgumentException.Create('Operator and requires 2 arguments.'); + Result := Args[0].AsScalar and Args[1].AsScalar; +end; + +class function TRtlFunctions.BitwiseOr(const Args: TArray): TDataValue; +begin + if Length(Args) <> 2 then + raise EArgumentException.Create('Operator or requires 2 arguments.'); + Result := Args[0].AsScalar or Args[1].AsScalar; +end; + +class function TRtlFunctions.BitwiseXor(const Args: TArray): TDataValue; +begin + if Length(Args) <> 2 then + raise EArgumentException.Create('Operator xor requires 2 arguments.'); + Result := Args[0].AsScalar xor Args[1].AsScalar; +end; + +class function TRtlFunctions.LeftShift(const Args: TArray): TDataValue; +begin + if Length(Args) <> 2 then + raise EArgumentException.Create('Operator shl requires 2 arguments.'); + Result := Args[0].AsScalar shl Args[1].AsScalar; +end; + +class function TRtlFunctions.RightShift(const Args: TArray): TDataValue; +begin + if Length(Args) <> 2 then + raise EArgumentException.Create('Operator shr requires 2 arguments.'); + Result := Args[0].AsScalar shr Args[1].AsScalar; end; { TRtlFunctions - Standard Functions } @@ -352,20 +389,22 @@ begin end; end; +class function TRtlFunctions.Round(const Arg: TScalar): TScalar; +begin + var val: Double := Arg; + Result := TScalar.FromInt64(System.Round(val)); +end; + class function TRtlFunctions.Trunc(const Arg: TScalar): TScalar; begin - case Arg.Kind of - TScalar.TKind.Ordinal: Result := Arg; // Trunc on an integer is a no-op - TScalar.TKind.Float: Result := TScalar.FromInt64(System.Trunc(Arg.Value.AsDouble)); - else - raise EArgumentException.Create('Trunc requires a numeric argument.'); - end; + var val: Double := Arg; + Result := TScalar.FromInt64(System.Trunc(val)); end; class function TRtlFunctions.Ceil(const Arg: TScalar): TScalar; begin case Arg.Kind of - TScalar.TKind.Ordinal: Result := Arg; // Ceil on an integer is a no-op + TScalar.TKind.Ordinal: Result := Arg; TScalar.TKind.Float: Result := TScalar.FromInt64(System.Math.Ceil(Arg.Value.AsDouble)); else raise EArgumentException.Create('Ceil requires a numeric argument.'); @@ -375,7 +414,7 @@ end; class function TRtlFunctions.Floor(const Arg: TScalar): TScalar; begin case Arg.Kind of - TScalar.TKind.Ordinal: Result := Arg; // Floor on an integer is a no-op + TScalar.TKind.Ordinal: Result := Arg; TScalar.TKind.Float: Result := TScalar.FromInt64(System.Math.Floor(Arg.Value.AsDouble)); else raise EArgumentException.Create('Floor requires a numeric argument.'); @@ -392,6 +431,38 @@ begin end; end; +// --- Date Constructors --- + +class function TRtlFunctions.Now(const Args: TArray): TDataValue; +begin + Result := TScalar.FromDateTime(System.SysUtils.Now); +end; + +class function TRtlFunctions.Date(const Args: TArray): TDataValue; + + function GetInt(const V: TDataValue; ArgIndex: Integer): Word; + begin + if V.AsScalar.Kind <> TScalar.TKind.Ordinal then + raise EArgumentException.CreateFmt('Date argument %d must be an Integer.', [ArgIndex]); + Result := V.AsScalar.Value.AsInt64; + end; + +begin + if Length(Args) = 3 then + begin + var y := GetInt(Args[0], 1); + var m := GetInt(Args[1], 2); + var d := GetInt(Args[2], 3); + Result := TScalar.FromDateTime(EncodeDate(y, m, d)); + end + else if Length(Args) = 0 then + Result := TScalar.FromDateTime(System.SysUtils.Date) + else + raise EArgumentException.Create('Date expects 0 or 3 arguments (Year, Month, Day).'); +end; + +// --- High-Order / Series --- + class function TRtlFunctions.Memoize(const Args: TArray): TDataValue; var funcToMemoize: TDataValue.TFunc; @@ -402,7 +473,6 @@ begin if Args[0].Kind <> vkMethod then raise EArgumentException.Create('The argument to Memoize must be a function.'); - // create a managed dictionary var cCache: TDataValue; cCache.FromObj(TDictionary.Create); @@ -518,9 +588,9 @@ begin begin item := sourceSeries.Items[i]; predicateResult := predicateFunc([TDataValue(item)]); - if (predicateResult.Kind = vkScalar) - and (predicateResult.AsScalar.Kind = TScalar.TKind.Ordinal) - and (predicateResult.AsScalar.Value.AsInt64 <> 0) then + + // Use the implicit Boolean operator of TScalar + if (predicateResult.Kind = vkScalar) and (Boolean(predicateResult.AsScalar)) then matchingIndices.Add(i); end; @@ -569,105 +639,91 @@ begin item := sourceSeries.Items[i]; predicateResult := predicateFunc([TDataValue(item)]); - if (predicateResult.Kind = vkScalar) - and (predicateResult.AsScalar.Kind = TScalar.TKind.Ordinal) - and (predicateResult.AsScalar.Value.AsInt64 <> 0) then + if (predicateResult.Kind = vkScalar) and (Boolean(predicateResult.AsScalar)) then begin - Result := TScalar.FromInt64(1); + Result := TScalar.FromBoolean(True); exit; end; end; - Result := TScalar.FromInt64(0); + Result := TScalar.FromBoolean(False); end; -{ TRtlFunctions - Static Specializations } +// --- Static Specializations --- -// --- Add --- +// Add class function TRtlFunctions.Add_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; begin Result := A + B; end; - class function TRtlFunctions.Add_Float_Float_Float(A, B: Double): Double; begin Result := A + B; end; - class function TRtlFunctions.Add_Ordinal_Float_Float(A: Int64; B: Double): Double; begin Result := A + B; end; - class function TRtlFunctions.Add_Float_Ordinal_Float(A: Double; B: Int64): Double; begin Result := A + B; end; -// --- Subtract --- +// Subtract class function TRtlFunctions.Subtract_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; begin Result := A - B; end; - class function TRtlFunctions.Subtract_Float_Float_Float(A, B: Double): Double; begin Result := A - B; end; - class function TRtlFunctions.Subtract_Ordinal_Float_Float(A: Int64; B: Double): Double; begin Result := A - B; end; - class function TRtlFunctions.Subtract_Float_Ordinal_Float(A: Double; B: Int64): Double; begin Result := A - B; end; -// --- Multiply --- +// Multiply class function TRtlFunctions.Multiply_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; begin Result := A * B; end; - class function TRtlFunctions.Multiply_Float_Float_Float(A, B: Double): Double; begin Result := A * B; end; - class function TRtlFunctions.Multiply_Ordinal_Float_Float(A: Int64; B: Double): Double; begin Result := A * B; end; - class function TRtlFunctions.Multiply_Float_Ordinal_Float(A: Double; B: Int64): Double; begin Result := A * B; end; -// --- Divide --- +// Divide class function TRtlFunctions.Divide_Ordinal_Ordinal_Float(A, B: Int64): Double; begin if B = 0 then raise EDivByZero.Create('Division by zero.'); Result := A / B; end; - class function TRtlFunctions.Divide_Float_Float_Float(A, B: Double): Double; begin if B = 0.0 then raise EDivByZero.Create('Division by zero.'); Result := A / B; end; - class function TRtlFunctions.Divide_Ordinal_Float_Float(A: Int64; B: Double): Double; begin if B = 0.0 then raise EDivByZero.Create('Division by zero.'); Result := A / B; end; - class function TRtlFunctions.Divide_Float_Ordinal_Float(A: Double; B: Int64): Double; begin if B = 0 then @@ -675,32 +731,35 @@ begin Result := A / B; end; -// --- Comparisons --- -(* Delphi bools: 0=False, 1=True. We return Int64 (0 or 1) *) +// Integer Math +class function TRtlFunctions.Div_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; +begin + Result := A div B; +end; // Delphi runtime handles EDivByZero for integers +class function TRtlFunctions.Mod_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; +begin + Result := A mod B; +end; +// Comparisons class function TRtlFunctions.Equal_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; begin Result := Ord(A = B); end; - class function TRtlFunctions.Equal_Float_Float_Ordinal(A, B: Double): Int64; begin - Result := Ord(A = B); // Note: Standard float comparison + Result := Ord(SameValue(A, B)); end; - class function TRtlFunctions.Equal_Ordinal_Float_Ordinal(A: Int64; B: Double): Int64; begin - Result := Ord(A = B); + Result := Ord(SameValue(A, B)); end; - class function TRtlFunctions.Equal_Float_Ordinal_Ordinal(A: Double; B: Int64): Int64; begin - Result := Ord(A = B); + Result := Ord(SameValue(A, B)); end; - class function TRtlFunctions.Equal_Keyword_Keyword_Ordinal(A, B: Int64): Int64; begin - // Keywords are passed as their Int64 index Result := Ord(A = B); end; @@ -708,22 +767,18 @@ class function TRtlFunctions.NotEqual_Ordinal_Ordinal_Ordinal(A, B: Int64): Int6 begin Result := Ord(A <> B); end; - class function TRtlFunctions.NotEqual_Float_Float_Ordinal(A, B: Double): Int64; begin - Result := Ord(A <> B); + Result := Ord(not SameValue(A, B)); end; - class function TRtlFunctions.NotEqual_Ordinal_Float_Ordinal(A: Int64; B: Double): Int64; begin - Result := Ord(A <> B); + Result := Ord(not SameValue(A, B)); end; - class function TRtlFunctions.NotEqual_Float_Ordinal_Ordinal(A: Double; B: Int64): Int64; begin - Result := Ord(A <> B); + Result := Ord(not SameValue(A, B)); end; - class function TRtlFunctions.NotEqual_Keyword_Keyword_Ordinal(A, B: Int64): Int64; begin Result := Ord(A <> B); @@ -733,17 +788,14 @@ class function TRtlFunctions.Less_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; begin Result := Ord(A < B); end; - class function TRtlFunctions.Less_Float_Float_Ordinal(A, B: Double): Int64; begin Result := Ord(A < B); end; - class function TRtlFunctions.Less_Ordinal_Float_Ordinal(A: Int64; B: Double): Int64; begin Result := Ord(A < B); end; - class function TRtlFunctions.Less_Float_Ordinal_Ordinal(A: Double; B: Int64): Int64; begin Result := Ord(A < B); @@ -753,17 +805,14 @@ class function TRtlFunctions.LessOrEqual_Ordinal_Ordinal_Ordinal(A, B: Int64): I begin Result := Ord(A <= B); end; - class function TRtlFunctions.LessOrEqual_Float_Float_Ordinal(A, B: Double): Int64; begin Result := Ord(A <= B); end; - class function TRtlFunctions.LessOrEqual_Ordinal_Float_Ordinal(A: Int64; B: Double): Int64; begin Result := Ord(A <= B); end; - class function TRtlFunctions.LessOrEqual_Float_Ordinal_Ordinal(A: Double; B: Int64): Int64; begin Result := Ord(A <= B); @@ -773,17 +822,14 @@ class function TRtlFunctions.Greater_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64 begin Result := Ord(A > B); end; - class function TRtlFunctions.Greater_Float_Float_Ordinal(A, B: Double): Int64; begin Result := Ord(A > B); end; - class function TRtlFunctions.Greater_Ordinal_Float_Ordinal(A: Int64; B: Double): Int64; begin Result := Ord(A > B); end; - class function TRtlFunctions.Greater_Float_Ordinal_Ordinal(A: Double; B: Int64): Int64; begin Result := Ord(A > B); @@ -793,29 +839,46 @@ class function TRtlFunctions.GreaterOrEqual_Ordinal_Ordinal_Ordinal(A, B: Int64) begin Result := Ord(A >= B); end; - class function TRtlFunctions.GreaterOrEqual_Float_Float_Ordinal(A, B: Double): Int64; begin Result := Ord(A >= B); end; - class function TRtlFunctions.GreaterOrEqual_Ordinal_Float_Ordinal(A: Int64; B: Double): Int64; begin Result := Ord(A >= B); end; - class function TRtlFunctions.GreaterOrEqual_Float_Ordinal_Ordinal(A: Double; B: Int64): Int64; begin Result := Ord(A >= B); end; -// --- Unary --- +// Bitwise Static +class function TRtlFunctions.And_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; +begin + Result := A and B; +end; +class function TRtlFunctions.Or_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; +begin + Result := A or B; +end; +class function TRtlFunctions.Xor_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; +begin + Result := A xor B; +end; +class function TRtlFunctions.Shl_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; +begin + Result := A shl B; +end; +class function TRtlFunctions.Shr_Ordinal_Ordinal_Ordinal(A, B: Int64): Int64; +begin + Result := A shr B; +end; +// Unary class function TRtlFunctions.Negate_Ordinal_Ordinal(A: Int64): Int64; begin Result := -A; end; - class function TRtlFunctions.Negate_Float_Float(A: Double): Double; begin Result := -A; @@ -823,27 +886,32 @@ end; class function TRtlFunctions.Not_Ordinal_Ordinal(A: Int64): Int64; begin - // Lisp-style 'not': 0 -> 1, everything else -> 0 Result := Ord(A = 0); -end; - -// --- Standard functions --- +end; // Logical NOT for Int64 +// Standard class function TRtlFunctions.Abs_Ordinal_Ordinal(A: Int64): Int64; begin Result := System.Abs(A); end; - class function TRtlFunctions.Abs_Float_Float(A: Double): Double; begin Result := System.Abs(A); end; -class function TRtlFunctions.Trunc_Ordinal_Ordinal(A: Int64): Int64; +class function TRtlFunctions.Round_Ordinal_Ordinal(A: Int64): Int64; begin - Result := A; // No-op + Result := A; +end; +class function TRtlFunctions.Round_Float_Ordinal(A: Double): Int64; +begin + Result := System.Round(A); end; +class function TRtlFunctions.Trunc_Ordinal_Ordinal(A: Int64): Int64; +begin + Result := A; +end; class function TRtlFunctions.Trunc_Float_Ordinal(A: Double): Int64; begin Result := System.Trunc(A); diff --git a/Src/AST/Myc.Ast.RTL.pas b/Src/AST/Myc.Ast.RTL.pas index c6f7e67..fadfbc9 100644 --- a/Src/AST/Myc.Ast.RTL.pas +++ b/Src/AST/Myc.Ast.RTL.pas @@ -332,9 +332,20 @@ begin var wrapper := CreateStaticWrapper(method, retType, argTypes, rttiParams); if Assigned(wrapper) then begin - // (* UPDATED: Capture IsPure from attribute *) methodRecord := TSpecializedMethod.Create(wrapper, retType, exportAttr.IsPure); RegisterStaticSpecialization(rtlName, argTypes, methodRecord); + + // If this is a generic wrapper (e.g. Abs(TScalar)) and no DynamicWrapper exists yet, + // use this wrapper as the interpreter fallback! + if (not Assigned(funcInfo.DynamicWrapper)) and (Length(argTypes) = 1) and (argTypes[0].Kind = stUnknown) then + begin + funcInfo.DynamicWrapper := wrapper; + end + else if (not Assigned(funcInfo.DynamicWrapper)) and (Length(argTypes) = 2) and (argTypes[0].Kind = stUnknown) then + begin + // Potentially handle 2-arg scalar wrappers here too if needed + // For now, Abs/Trunc are 1-arg TScalar->TScalar, which matches stUnknown check above. + end; end else raise ENotImplemented.CreateFmt('Static native method wrapper for %s() not implemented', [method.Name]); diff --git a/Src/Data/Myc.Data.Scalar.pas b/Src/Data/Myc.Data.Scalar.pas index 918ac30..2dbfd82 100644 --- a/Src/Data/Myc.Data.Scalar.pas +++ b/Src/Data/Myc.Data.Scalar.pas @@ -3,9 +3,11 @@ unit Myc.Data.Scalar; interface uses - System.SysUtils, - System.Generics.Collections, - System.Generics.Defaults, + system.sysutils, + system.generics.collections, + system.generics.defaults, + system.math, + system.dateutils, Myc.Utils, Myc.Data.Decimal, Myc.Data.Series, @@ -19,17 +21,36 @@ type public type // Defines the underlying storage kinds for scalar values - TKind = (Ordinal, Float, Keyword); + TKind = (Ordinal, Float, Keyword, Boolean, DateTime); // The 8-byte storage for the scalar value TValue = record case TKind of - TKind.Ordinal, TKind.Keyword: (AsInt64: Int64); // Ordinal and Keyword index - TKind.Float: (AsDouble: Double); + TKind.Ordinal, TKind.Keyword, TKind.Boolean: (AsInt64: Int64); + TKind.Float, TKind.DateTime: (AsDouble: Double); end; - TBinaryOp = (Add, Subtract, Multiply, Divide, Equal, NotEqual, Less, Greater, LessOrEqual, GreaterOrEqual); - TUnaryOp = (Negate, &Not); + TBinaryOp = ( + Add, + Subtract, + Multiply, + Divide, + IntDivide, + Modulus, + Equal, + NotEqual, + Less, + Greater, + LessOrEqual, + GreaterOrEqual, + BitwiseAnd, + BitwiseOr, + BitwiseXor, + LeftShift, + RightShift + ); + + TUnaryOp = (Negate, Positive, &Not); TKindHelper = record helper for TKind public @@ -55,13 +76,19 @@ type class function FromInt64(AValue: Int64): TScalar; static; inline; class function FromDouble(AValue: Double): TScalar; static; inline; class function FromKeyword(const AValue: IKeyword): TScalar; static; inline; + class function FromBoolean(AValue: Boolean): TScalar; static; inline; + class function FromDateTime(AValue: TDateTime): TScalar; static; inline; // Implicit casts for core types. class operator Implicit(AValue: Int64): TScalar; overload; inline; class operator Implicit(AValue: Double): TScalar; overload; inline; class operator Implicit(const AValue: IKeyword): TScalar; overload; inline; - class operator Implicit(const A: TScalar): Double; overload; - class operator Implicit(const A: TScalar): Int64; overload; + class operator Implicit(AValue: Boolean): TScalar; overload; inline; + + class operator Implicit(const A: TScalar): Double; overload; inline; + class operator Implicit(const A: TScalar): Int64; overload; inline; + class operator Implicit(const A: TScalar): Boolean; overload; inline; + class operator Implicit(const A: TScalar): TDateTime; overload; inline; class function StringToKind(const AName: string): TKind; static; function ToString: String; @@ -72,20 +99,34 @@ type class function TryBinaryOperation(Op: TBinaryOp; const A, B: TScalar; out Res: TScalar): Boolean; static; class function TryUnaryOperation(Op: TUnaryOp; const A: TScalar; out Res: TScalar): Boolean; static; - class operator Add(const A, B: TScalar): TScalar; - class operator Divide(const A, B: TScalar): TScalar; - class operator Equal(const A, B: TScalar): Boolean; - class operator GreaterThan(const A, B: TScalar): Boolean; - class operator GreaterThanOrEqual(const A, B: TScalar): Boolean; - class operator LessThan(const A, B: TScalar): Boolean; - class operator LessThanOrEqual(const A, B: TScalar): Boolean; - class operator LogicalNot(const A: TScalar): TScalar; - class operator Multiply(const A, B: TScalar): TScalar; - class operator Negative(const A: TScalar): TScalar; - class operator NotEqual(const A, B: TScalar): Boolean; - class operator Round(const A: TScalar): TScalar; - class operator Subtract(const A, B: TScalar): TScalar; - class operator Trunc(const A: TScalar): TScalar; + // Operators + class operator Add(const A, B: TScalar): TScalar; inline; + class operator Subtract(const A, B: TScalar): TScalar; inline; + class operator Multiply(const A, B: TScalar): TScalar; inline; + class operator Divide(const A, B: TScalar): TScalar; inline; + + class operator IntDivide(const A, B: TScalar): TScalar; inline; + class operator Modulus(const A, B: TScalar): TScalar; inline; + class operator Positive(const A: TScalar): TScalar; inline; + + class operator Equal(const A, B: TScalar): Boolean; inline; + class operator NotEqual(const A, B: TScalar): Boolean; inline; + class operator GreaterThan(const A, B: TScalar): Boolean; inline; + class operator GreaterThanOrEqual(const A, B: TScalar): Boolean; inline; + class operator LessThan(const A, B: TScalar): Boolean; inline; + class operator LessThanOrEqual(const A, B: TScalar): Boolean; inline; + + class operator BitwiseAnd(const A, B: TScalar): TScalar; inline; + class operator BitwiseOr(const A, B: TScalar): TScalar; inline; + class operator BitwiseXor(const A, B: TScalar): TScalar; inline; + class operator LeftShift(const A, B: TScalar): TScalar; inline; + class operator RightShift(const A, B: TScalar): TScalar; inline; + + class operator LogicalNot(const A: TScalar): TScalar; inline; + class operator Negative(const A: TScalar): TScalar; inline; + + class operator Round(const A: TScalar): TScalar; inline; + class operator Trunc(const A: TScalar): TScalar; inline; end; // An array of scalar values of the same kind. @@ -236,6 +277,20 @@ begin Result.Value.AsInt64 := AValue.Idx end; +class function TScalar.FromBoolean(AValue: Boolean): TScalar; +begin + Result.Kind := TKind.Boolean; + Result.Value.AsInt64 := Ord(AValue); +end; + +class function TScalar.FromDateTime(AValue: TDateTime): TScalar; +begin + Result.Kind := TKind.DateTime; + Result.Value.AsDouble := AValue; +end; + +// --- Implicit Conversions --- + class operator TScalar.Implicit(AValue: Double): TScalar; begin Result.Kind := TKind.Float; @@ -256,13 +311,20 @@ begin Result.Value.AsInt64 := AValue.Idx end; +class operator TScalar.Implicit(AValue: Boolean): TScalar; +begin + Result.Kind := TKind.Boolean; + Result.Value.AsInt64 := Ord(AValue); +end; + class operator TScalar.Implicit(const A: TScalar): Double; begin case A.Kind of TKind.Ordinal: Result := A.Value.AsInt64; TKind.Float: Result := A.Value.AsDouble; + TKind.DateTime: Result := A.Value.AsDouble; else - raise EInvalidCast.Create('Cannot implicitly convert Keyword to Double'); + raise EInvalidCast.CreateFmt('Cannot implicitly convert %s to Double', [A.Kind.ToString]); end; end; @@ -270,33 +332,111 @@ class operator TScalar.Implicit(const A: TScalar): Int64; begin case A.Kind of TKind.Ordinal: Result := A.Value.AsInt64; + TKind.Boolean: Result := A.Value.AsInt64; else raise EInvalidCast.CreateFmt('Cannot implicitly convert %s to Int64', [A.Kind.ToString]); end; end; +class operator TScalar.Implicit(const A: TScalar): Boolean; +begin + case A.Kind of + TKind.Boolean, TKind.Ordinal: Result := A.Value.AsInt64 <> 0; + else + raise EInvalidCast.CreateFmt('Cannot implicitly convert %s to Boolean', [A.Kind.ToString]); + end; +end; + +class operator TScalar.Implicit(const A: TScalar): TDateTime; +begin + case A.Kind of + TKind.DateTime: Result := A.Value.AsDouble; + else + raise EInvalidCast.CreateFmt('Cannot implicitly convert %s to TDateTime', [A.Kind.ToString]); + end; +end; + +// --- Support Checks --- + class function TScalar.IsBinaryOperatorSupported(Op: TBinaryOp; A, B: TKind): Boolean; begin - // Deny all operations if either operand is Keyword... + // Keyword logic if (A = TKind.Keyword) or (B = TKind.Keyword) then begin - // ...except for Equality checks Result := (Op in [TBinaryOp.Equal, TBinaryOp.NotEqual]); exit; end; - // Default for Ordinal/Float + // DateTime logic + if (A = TKind.DateTime) or (B = TKind.DateTime) then + begin + case Op of + TBinaryOp.Add: + // Date + Num or Num + Date + Result := + (A in [TKind.DateTime, TKind.Ordinal, TKind.Float]) + and (B in [TKind.DateTime, TKind.Ordinal, TKind.Float]) + and not ((A = TKind.DateTime) and (B = TKind.DateTime)); + + TBinaryOp.Subtract: + // Date - Num or Date - Date + Result := (A = TKind.DateTime) and (B in [TKind.DateTime, TKind.Ordinal, TKind.Float]); + + TBinaryOp.Equal, TBinaryOp.NotEqual, TBinaryOp.Less, TBinaryOp.Greater, TBinaryOp.LessOrEqual, TBinaryOp.GreaterOrEqual: + Result := True; + + else + Result := False; + end; + exit; + end; + + // Boolean Logic + if (A = TKind.Boolean) or (B = TKind.Boolean) then + begin + if (A = TKind.Boolean) and (B = TKind.Boolean) then + begin + Result := Op in [TBinaryOp.Equal, TBinaryOp.NotEqual, TBinaryOp.BitwiseAnd, TBinaryOp.BitwiseOr, TBinaryOp.BitwiseXor]; + exit; + end; + Result := Op in [TBinaryOp.Equal, TBinaryOp.NotEqual]; + exit; + end; + + // Bitwise/Int Math requires Ordinals + if Op + in [ + TBinaryOp.IntDivide, + TBinaryOp.Modulus, + TBinaryOp.BitwiseAnd, + TBinaryOp.BitwiseOr, + TBinaryOp.BitwiseXor, + TBinaryOp.LeftShift, + TBinaryOp.RightShift] then + begin + Result := (A = TKind.Ordinal) and (B = TKind.Ordinal); + exit; + end; + + // Default Numeric Result := True; end; class function TScalar.IsUnaryOperatorSupported(Op: TUnaryOp; A: TKind): Boolean; begin - // Deny all unary ops for Keywords - if (A = TKind.Keyword) then + if A = TKind.Keyword then exit(False); - // Deny 'not' for Float - if (Op = TUnaryOp.Not) and (A = TKind.Float) then + if A = TKind.Boolean then + begin + Result := (Op = TUnaryOp.Not); + exit; + end; + + if A = TKind.DateTime then + exit(False); + + if (Op = TUnaryOp.Not) and (A <> TKind.Ordinal) then exit(False); Result := True; @@ -310,6 +450,10 @@ begin Result := TKind.Float else if SameText(AName, 'Keyword') then Result := TKind.Keyword + else if SameText(AName, 'Boolean') then + Result := TKind.Boolean + else if SameText(AName, 'DateTime') then + Result := TKind.DateTime else raise EArgumentException.CreateFmt('Unknown scalar type name: "%s"', [AName]); end; @@ -320,6 +464,8 @@ begin TKind.Ordinal: Result := IntToStr(Value.AsInt64); TKind.Float: Result := FloatToStr(Value.AsDouble); TKind.Keyword: Result := ':' + TKeywordRegistry.GetName(Value.AsInt64); + TKind.Boolean: Result := BoolToStr(Value.AsInt64 <> 0, True); + TKind.DateTime: Result := DateToISO8601(Value.AsDouble, True); else Result := '[Unknown Scalar]'; end; @@ -329,22 +475,27 @@ class function TScalar.TryBinaryOperation(Op: TBinaryOp; const A, B: TScalar; ou begin if not IsBinaryOperatorSupported(Op, A.Kind, B.Kind) then exit(False); - try case Op of TBinaryOp.Add: Res := A + B; TBinaryOp.Subtract: Res := A - B; TBinaryOp.Multiply: Res := A * B; TBinaryOp.Divide: Res := A / B; - TBinaryOp.Equal: Res := TScalar.FromInt64(Integer(A = B)); - TBinaryOp.NotEqual: Res := TScalar.FromInt64(Integer(A <> B)); - TBinaryOp.Less: Res := TScalar.FromInt64(Integer(A < B)); - TBinaryOp.Greater: Res := TScalar.FromInt64(Integer(A > B)); - TBinaryOp.LessOrEqual: Res := TScalar.FromInt64(Integer(A <= B)); - TBinaryOp.GreaterOrEqual: Res := TScalar.FromInt64(Integer(A >= B)); + TBinaryOp.IntDivide: Res := A div B; + TBinaryOp.Modulus: Res := A mod B; + TBinaryOp.Equal: Res := TScalar.FromBoolean(A = B); + TBinaryOp.NotEqual: Res := TScalar.FromBoolean(A <> B); + TBinaryOp.Less: Res := TScalar.FromBoolean(A < B); + TBinaryOp.Greater: Res := TScalar.FromBoolean(A > B); + TBinaryOp.LessOrEqual: Res := TScalar.FromBoolean(A <= B); + TBinaryOp.GreaterOrEqual: Res := TScalar.FromBoolean(A >= B); + TBinaryOp.BitwiseAnd: Res := A and B; + TBinaryOp.BitwiseOr: Res := A or B; + TBinaryOp.BitwiseXor: Res := A xor B; + TBinaryOp.LeftShift: Res := A shl B; + TBinaryOp.RightShift: Res := A shr B; else - Result := False; - exit; + exit(False); end; Result := True; except @@ -359,10 +510,10 @@ begin try case Op of TUnaryOp.Negate: Res := -A; + TUnaryOp.Positive: Res := +A; TUnaryOp.Not: Res := not A; else - Result := False; - exit; + exit(False); end; Result := True; except @@ -370,80 +521,163 @@ begin end; end; +// --- Operators --- + class operator TScalar.Add(const A, B: TScalar): TScalar; begin + // Date Math + if (A.Kind = TKind.DateTime) or (B.Kind = TKind.DateTime) then + begin + if (A.Kind = TKind.DateTime) and (B.Kind = TKind.DateTime) then + raise EArgumentException.Create('Cannot add two DateTimes.'); + + var dateVal: Double; + var numVal: Double; + + if A.Kind = TKind.DateTime then + begin + dateVal := A.Value.AsDouble; + numVal := Double(B); + end + else + begin + dateVal := B.Value.AsDouble; + numVal := Double(A); + end; + + Result.Kind := TKind.DateTime; + Result.Value.AsDouble := dateVal + numVal; + exit; + end; + if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then Result := A.Value.AsInt64 + B.Value.AsInt64 - else if (A.Kind = TKind.Float) or (B.Kind = TKind.Float) then + else if (A.Kind in [TKind.Ordinal, TKind.Float]) and (B.Kind in [TKind.Ordinal, TKind.Float]) then begin - var valA, valB: Double; - valA := A; // Use implicit cast - valB := B; // Use implicit cast + var valA: Double := A; + var valB: Double := B; Result := valA + valB; end else raise EArgumentException.CreateFmt('Operator Add not supported for types %s and %s', [A.Kind.ToString, B.Kind.ToString]); end; +class operator TScalar.Subtract(const A, B: TScalar): TScalar; +begin + // Date Math + if A.Kind = TKind.DateTime then + begin + var dateA := A.Value.AsDouble; + if B.Kind = TKind.DateTime then + begin + Result.Kind := TKind.Float; + Result.Value.AsDouble := dateA - B.Value.AsDouble; + end + else if B.Kind in [TKind.Ordinal, TKind.Float] then + begin + Result.Kind := TKind.DateTime; + Result.Value.AsDouble := dateA - Double(B); + end + else + raise EArgumentException.Create('Invalid operand for Date subtraction.'); + exit; + end; + + if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then + Result := A.Value.AsInt64 - B.Value.AsInt64 + else if (A.Kind in [TKind.Ordinal, TKind.Float]) and (B.Kind in [TKind.Ordinal, TKind.Float]) then + begin + var valA: Double := A; + var valB: Double := B; + Result := valA - valB; + end + else + raise EArgumentException.CreateFmt('Operator Subtract not supported for types %s and %s', [A.Kind.ToString, B.Kind.ToString]); +end; + +class operator TScalar.Multiply(const A, B: TScalar): TScalar; +begin + if (A.Kind in [TKind.DateTime, TKind.Keyword, TKind.Boolean]) or (B.Kind in [TKind.DateTime, TKind.Keyword, TKind.Boolean]) then + raise EArgumentException.CreateFmt('Operator Multiply not supported for types %s and %s', [A.Kind.ToString, B.Kind.ToString]); + + if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then + Result := A.Value.AsInt64 * B.Value.AsInt64 + else + begin + var valA: Double := A; + var valB: Double := B; + Result := valA * valB; + end; +end; + class operator TScalar.Divide(const A, B: TScalar): TScalar; begin - // Division *always* promotes to Float, unless types are invalid - if (A.Kind = TKind.Keyword) or (B.Kind = TKind.Keyword) then + if (A.Kind in [TKind.DateTime, TKind.Keyword, TKind.Boolean]) or (B.Kind in [TKind.DateTime, TKind.Keyword, TKind.Boolean]) then raise EArgumentException.CreateFmt('Operator Divide not supported for types %s and %s', [A.Kind.ToString, B.Kind.ToString]); - var valA, valB: Double; - valA := A; // Use implicit cast - valB := B; // Use implicit cast + var valB: Double := B; + if valB = 0.0 then + raise EDivByZero.Create('Division by zero'); + + var valA: Double := A; Result := valA / valB; end; +class operator TScalar.IntDivide(const A, B: TScalar): TScalar; +begin + if (A.Kind <> TKind.Ordinal) or (B.Kind <> TKind.Ordinal) then + raise EArgumentException.Create('Operator div requires Ordinal arguments.'); + + if B.Value.AsInt64 = 0 then + raise EDivByZero.Create('Division by zero'); + + Result.Kind := TKind.Ordinal; + Result.Value.AsInt64 := A.Value.AsInt64 div B.Value.AsInt64; +end; + +class operator TScalar.Modulus(const A, B: TScalar): TScalar; +begin + if (A.Kind <> TKind.Ordinal) or (B.Kind <> TKind.Ordinal) then + raise EArgumentException.Create('Operator mod requires Ordinal arguments.'); + + if B.Value.AsInt64 = 0 then + raise EDivByZero.Create('Division by zero'); + + Result.Kind := TKind.Ordinal; + Result.Value.AsInt64 := A.Value.AsInt64 mod B.Value.AsInt64; +end; + class operator TScalar.Equal(const A, B: TScalar): Boolean; begin - // Must be same kind to be equal (e.g., Ordinal(5) <> Keyword(5)) - if A.Kind <> B.Kind then - exit(False); - - case A.Kind of - TKind.Ordinal, TKind.Keyword: Result := A.Value.AsInt64 = B.Value.AsInt64; - TKind.Float: Result := A.Value.AsDouble = B.Value.AsDouble; + if A.Kind = B.Kind then + begin + case A.Kind of + TKind.Ordinal, TKind.Keyword, TKind.Boolean: Result := A.Value.AsInt64 = B.Value.AsInt64; + TKind.Float, TKind.DateTime: Result := SameValue(A.Value.AsDouble, B.Value.AsDouble); + else + Result := False; + end; + end + else if (A.Kind in [TKind.Ordinal, TKind.Float]) and (B.Kind in [TKind.Ordinal, TKind.Float]) then + Result := SameValue(Double(A), Double(B)) else Result := False; - end; end; -class operator TScalar.GreaterThan(const A, B: TScalar): Boolean; +class operator TScalar.NotEqual(const A, B: TScalar): Boolean; begin - if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then - Result := A.Value.AsInt64 > B.Value.AsInt64 - else if (A.Kind = TKind.Float) or (B.Kind = TKind.Float) then - begin - var valA, valB: Double; - valA := A; - valB := B; - Result := valA > valB; - end - else - raise EArgumentException.CreateFmt('Operator GreaterThan not supported for types %s and %s', [A.Kind.ToString, B.Kind.ToString]); -end; - -class operator TScalar.GreaterThanOrEqual(const A, B: TScalar): Boolean; -begin - Result := not (A < B); + Result := not (A = B); end; class operator TScalar.LessThan(const A, B: TScalar): Boolean; begin + if (A.Kind = TKind.Keyword) or (B.Kind = TKind.Keyword) or (A.Kind = TKind.Boolean) or (B.Kind = TKind.Boolean) then + raise EArgumentException.Create('Order comparison not supported for Keyword/Boolean.'); + if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then Result := A.Value.AsInt64 < B.Value.AsInt64 - else if (A.Kind = TKind.Float) or (B.Kind = TKind.Float) then - begin - var valA, valB: Double; - valA := A; - valB := B; - Result := valA < valB; - end else - raise EArgumentException.CreateFmt('Operator LessThan not supported for types %s and %s', [A.Kind.ToString, B.Kind.ToString]); + Result := Double(A) < Double(B); end; class operator TScalar.LessThanOrEqual(const A, B: TScalar): Boolean; @@ -451,30 +685,74 @@ begin Result := not (A > B); end; -class operator TScalar.LogicalNot(const A: TScalar): TScalar; +class operator TScalar.GreaterThan(const A, B: TScalar): Boolean; begin - if A.Kind <> TKind.Ordinal then - raise EArgumentException.CreateFmt('Operator Not not supported for type %s', [A.Kind.ToString]); + if (A.Kind = TKind.Keyword) or (B.Kind = TKind.Keyword) or (A.Kind = TKind.Boolean) or (B.Kind = TKind.Boolean) then + raise EArgumentException.Create('Order comparison not supported for Keyword/Boolean.'); - if A.Value.AsInt64 = 0 then - Result := 1 + if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then + Result := A.Value.AsInt64 > B.Value.AsInt64 else - Result := 0; + Result := Double(A) > Double(B); end; -class operator TScalar.Multiply(const A, B: TScalar): TScalar; +class operator TScalar.GreaterThanOrEqual(const A, B: TScalar): Boolean; begin - if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then - Result := A.Value.AsInt64 * B.Value.AsInt64 - else if (A.Kind = TKind.Float) or (B.Kind = TKind.Float) then - begin - var valA, valB: Double; - valA := A; - valB := B; - Result := valA * valB; - end + Result := not (A < B); +end; + +class operator TScalar.BitwiseAnd(const A, B: TScalar): TScalar; +begin + if (A.Kind = TKind.Boolean) and (B.Kind = TKind.Boolean) then + Result := FromBoolean((A.Value.AsInt64 <> 0) and (B.Value.AsInt64 <> 0)) + else if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then + Result := FromInt64(A.Value.AsInt64 and B.Value.AsInt64) else - raise EArgumentException.CreateFmt('Operator Multiply not supported for types %s and %s', [A.Kind.ToString, B.Kind.ToString]); + raise EArgumentException.Create('Operator "and" requires Ordinal or Boolean arguments.'); +end; + +class operator TScalar.BitwiseOr(const A, B: TScalar): TScalar; +begin + if (A.Kind = TKind.Boolean) and (B.Kind = TKind.Boolean) then + Result := FromBoolean((A.Value.AsInt64 <> 0) or (B.Value.AsInt64 <> 0)) + else if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then + Result := FromInt64(A.Value.AsInt64 or B.Value.AsInt64) + else + raise EArgumentException.Create('Operator "or" requires Ordinal or Boolean arguments.'); +end; + +class operator TScalar.BitwiseXor(const A, B: TScalar): TScalar; +begin + if (A.Kind = TKind.Boolean) and (B.Kind = TKind.Boolean) then + Result := FromBoolean((A.Value.AsInt64 <> 0) xor (B.Value.AsInt64 <> 0)) + else if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then + Result := FromInt64(A.Value.AsInt64 xor B.Value.AsInt64) + else + raise EArgumentException.Create('Operator "xor" requires Ordinal or Boolean arguments.'); +end; + +class operator TScalar.LeftShift(const A, B: TScalar): TScalar; +begin + if (A.Kind <> TKind.Ordinal) or (B.Kind <> TKind.Ordinal) then + raise EArgumentException.Create('Operator shl requires Ordinal arguments.'); + Result := FromInt64(A.Value.AsInt64 shl B.Value.AsInt64); +end; + +class operator TScalar.RightShift(const A, B: TScalar): TScalar; +begin + if (A.Kind <> TKind.Ordinal) or (B.Kind <> TKind.Ordinal) then + raise EArgumentException.Create('Operator shr requires Ordinal arguments.'); + Result := FromInt64(A.Value.AsInt64 shr B.Value.AsInt64); +end; + +class operator TScalar.LogicalNot(const A: TScalar): TScalar; +begin + if A.Kind = TKind.Boolean then + Result := FromBoolean(A.Value.AsInt64 = 0) + else if A.Kind = TKind.Ordinal then + Result := FromBoolean(A.Value.AsInt64 = 0) + else + raise EArgumentException.CreateFmt('Operator Not not supported for type %s', [A.Kind.ToString]); end; class operator TScalar.Negative(const A: TScalar): TScalar; @@ -488,38 +766,24 @@ begin end; end; -class operator TScalar.NotEqual(const A, B: TScalar): Boolean; +class operator TScalar.Positive(const A: TScalar): TScalar; begin - Result := not (A = B); + if A.Kind in [TKind.Ordinal, TKind.Float] then + Result := A + else + raise EArgumentException.CreateFmt('Operator Positive not supported for type %s', [A.Kind.ToString]); end; class operator TScalar.Round(const A: TScalar): TScalar; begin - var val: Double; - val := A; // Implicit cast handles Ordinal or Float - Result := System.Round(val); -end; - -class operator TScalar.Subtract(const A, B: TScalar): TScalar; -begin - if (A.Kind = TKind.Ordinal) and (B.Kind = TKind.Ordinal) then - Result := A.Value.AsInt64 - B.Value.AsInt64 - else if (A.Kind = TKind.Float) or (B.Kind = TKind.Float) then - begin - var valA, valB: Double; - valA := A; - valB := B; - Result := valA - valB; - end - else - raise EArgumentException.CreateFmt('Operator Subtract not supported for types %s and %s', [A.Kind.ToString, B.Kind.ToString]); + var val: Double := A; + Result := FromInt64(System.Round(val)); end; class operator TScalar.Trunc(const A: TScalar): TScalar; begin - var val: Double; - val := A; // Implicit cast handles Ordinal or Float - Result := System.Trunc(val); + var val: Double := A; + Result := FromInt64(System.Trunc(val)); end; { TScalarArray } @@ -623,6 +887,8 @@ begin TScalar.TKind.Ordinal: Result := 'Ordinal'; TScalar.TKind.Float: Result := 'Float'; TScalar.TKind.Keyword: Result := 'Keyword'; + TScalar.TKind.Boolean: Result := 'Boolean'; + TScalar.TKind.DateTime: Result := 'DateTime'; else Result := 'unknown'; end; @@ -637,12 +903,19 @@ begin TScalar.TBinaryOp.Subtract: Result := '-'; TScalar.TBinaryOp.Multiply: Result := '*'; TScalar.TBinaryOp.Divide: Result := '/'; + TScalar.TBinaryOp.IntDivide: Result := 'div'; + TScalar.TBinaryOp.Modulus: Result := 'mod'; TScalar.TBinaryOp.Equal: Result := '='; TScalar.TBinaryOp.NotEqual: Result := '<>'; TScalar.TBinaryOp.Less: Result := '<'; TScalar.TBinaryOp.Greater: Result := '>'; TScalar.TBinaryOp.LessOrEqual: Result := '<='; TScalar.TBinaryOp.GreaterOrEqual: Result := '>='; + TScalar.TBinaryOp.BitwiseAnd: Result := 'and'; + TScalar.TBinaryOp.BitwiseOr: Result := 'or'; + TScalar.TBinaryOp.BitwiseXor: Result := 'xor'; + TScalar.TBinaryOp.LeftShift: Result := 'shl'; + TScalar.TBinaryOp.RightShift: Result := 'shr'; else Result := '?'; end; @@ -654,6 +927,7 @@ function TScalar.TUnaryOpHelper.ToString: string; begin case Self of TScalar.TUnaryOp.Negate: Result := '-'; + TScalar.TUnaryOp.Positive: Result := '+'; TScalar.TUnaryOp.Not: Result := 'not'; else Result := '?'; diff --git a/Test/AST/Test.Myc.Ast.RTL.pas b/Test/AST/Test.Myc.Ast.RTL.pas new file mode 100644 index 0000000..725bf05 --- /dev/null +++ b/Test/AST/Test.Myc.Ast.RTL.pas @@ -0,0 +1,219 @@ +unit Test.Myc.Ast.RTL; + +interface + +uses + DUnitX.TestFramework, + System.SysUtils, + System.Generics.Collections, + System.Math, + System.DateUtils, + Myc.Data.Scalar, + Myc.Data.Value, + Myc.Ast.Types, + Myc.Ast.Scope, + Myc.Ast.RTL, + Myc.Ast.RTL.Core; + +type + [TestFixture] + TTestMycAstRTL = class + private + FScope: IExecutionScope; + + function Call(const Name: string; const Args: array of TDataValue): TDataValue; + function ValI(V: Int64): TDataValue; + function ValF(V: Double): TDataValue; + public + [Setup] + procedure Setup; + + // --- Registration --- + [Test] + [IgnoreMemoryLeaks] + procedure RTL_Registration_SymbolsArePresent; + + // --- Floating Point Math --- + [Test] + [TestCase('Add_FF', '+,1.5,2.5,4.0')] + [TestCase('Div_FF', '/,10,2,5.0')] + procedure RTL_Math_Float(const Op: string; A, B, Expected: Double); + + // --- Integer Math (New) --- + [Test] + [TestCase('Div_Int', 'div,10,3,3')] + [TestCase('Mod_Int', 'mod,10,3,1')] + procedure RTL_Math_Integer(const Op: string; A, B, Expected: Int64); + + // --- Bitwise Operations (New) --- + [Test] + [TestCase('BitAnd', 'and,3,2,2')] // 011 & 010 = 010 (2) + [TestCase('BitOr', 'or,1,2,3')] // 001 | 010 = 011 (3) + [TestCase('BitXor', 'xor,3,1,2')] // 011 ^ 001 = 010 (2) + [TestCase('Shl', 'shl,1,2,4')] // 1 << 2 = 4 + [TestCase('Shr', 'shr,4,1,2')] // 4 >> 1 = 2 + procedure RTL_Bitwise(const Op: string; A, B, Expected: Int64); + + // --- Rounding (New) --- + [Test] + [TestCase('Round_Up', '3.6,4')] + [TestCase('Round_Down', '3.4,3')] + [TestCase('Round_Mid', '3.5,4')] // Banker's rounding or standard? Delphi default is Banker's. + procedure RTL_Round_Works(A: Double; Expected: Int64); + + // --- Comparisons --- + [Test] + [TestCase('Eq_True', '=,10,10,1')] + [TestCase('Neq_True', '<>,10,20,1')] + procedure RTL_Comparison_Ordinals(const Op: string; A, B: Int64; ExpectedBool: Int64); + + // --- DateTime Logic (New) --- + [Test] + procedure RTL_DateTime_ConstructionAndMath; + + // --- Error Handling --- + [Test] + procedure RTL_DivByZero_ThrowException; + end; + +implementation + +uses + Myc.Ast; + +{ TTestMycAstRTL } + +procedure TTestMycAstRTL.Setup; +begin + FScope := TAst.CreateScope(nil, nil, False); + Myc.Ast.RTL.RegisterRtlFunctions(FScope); +end; + +// --- Helpers --- + +function TTestMycAstRTL.ValI(V: Int64): TDataValue; +begin + Result := TDataValue(TScalar.FromInt64(V)); +end; + +function TTestMycAstRTL.ValF(V: Double): TDataValue; +begin + Result := TDataValue(TScalar.FromDouble(V)); +end; + +function TTestMycAstRTL.Call(const Name: string; const Args: array of TDataValue): TDataValue; +var + addr: TResolvedAddress; + funcVal: TDataValue; + func: TDataValue.TFunc; + argArray: TArray; + i: Integer; +begin + addr := FScope.Resolve(Name); + Assert.AreNotEqual(TAddressKind.akUnresolved, addr.Kind, 'Function not found: ' + Name); + + funcVal := FScope[addr]; + func := funcVal.AsMethod(); + + SetLength(argArray, Length(Args)); + for i := 0 to High(Args) do + argArray[i] := Args[i]; + + Result := func(argArray); +end; + +// --- Tests --- + +procedure TTestMycAstRTL.RTL_Registration_SymbolsArePresent; +begin + Assert.AreNotEqual(TAddressKind.akUnresolved, FScope.Resolve('div').Kind); + Assert.AreNotEqual(TAddressKind.akUnresolved, FScope.Resolve('mod').Kind); + Assert.AreNotEqual(TAddressKind.akUnresolved, FScope.Resolve('Date').Kind); + Assert.AreNotEqual(TAddressKind.akUnresolved, FScope.Resolve('Round').Kind); +end; + +procedure TTestMycAstRTL.RTL_Math_Float(const Op: string; A, B, Expected: Double); +var + res: TDataValue; +begin + res := Call(Op, [ValF(A), ValF(B)]); + Assert.AreEqual(TDataValueKind.vkScalar, res.Kind); + Assert.AreEqual(Expected, res.AsScalar.Value.AsDouble, 0.00001); +end; + +procedure TTestMycAstRTL.RTL_Math_Integer(const Op: string; A, B, Expected: Int64); +var + res: TDataValue; +begin + res := Call(Op, [ValI(A), ValI(B)]); + Assert.AreEqual(TDataValueKind.vkScalar, res.Kind); + Assert.AreEqual(TScalar.TKind.Ordinal, res.AsScalar.Kind); + Assert.AreEqual(Expected, res.AsScalar.Value.AsInt64); +end; + +procedure TTestMycAstRTL.RTL_Bitwise(const Op: string; A, B, Expected: Int64); +var + res: TDataValue; +begin + res := Call(Op, [ValI(A), ValI(B)]); + Assert.AreEqual(TScalar.TKind.Ordinal, res.AsScalar.Kind); + Assert.AreEqual(Expected, res.AsScalar.Value.AsInt64); +end; + +procedure TTestMycAstRTL.RTL_Round_Works(A: Double; Expected: Int64); +var + res: TDataValue; +begin + res := Call('Round', [ValF(A)]); + Assert.AreEqual(TScalar.TKind.Ordinal, res.AsScalar.Kind); + Assert.AreEqual(Expected, res.AsScalar.Value.AsInt64); +end; + +procedure TTestMycAstRTL.RTL_Comparison_Ordinals(const Op: string; A, B: Int64; ExpectedBool: Int64); +var + res: TDataValue; +begin + res := Call(Op, [ValI(A), ValI(B)]); + // Note: TScalar.Equal/NotEqual returns Boolean Kind now! + // The test case expects integer 0 or 1, so we convert or check bool. + // TScalar.Implicit(Boolean) -> Int64 (0/1) works. + + Assert.AreEqual(TDataValueKind.vkScalar, res.Kind); + Assert.AreEqual(TScalar.TKind.Boolean, res.AsScalar.Kind); + + var asInt: Int64 := res.AsScalar; // Implicit conversion + Assert.AreEqual(ExpectedBool, asInt); +end; + +procedure TTestMycAstRTL.RTL_DateTime_ConstructionAndMath; +var + d, d2, diff: TDataValue; + expectedDate: TDateTime; +begin + // 1. Test Date(Y, M, D) + expectedDate := EncodeDate(2023, 10, 5); + d := Call('Date', [ValI(2023), ValI(10), ValI(5)]); + + Assert.AreEqual(TScalar.TKind.DateTime, d.AsScalar.Kind); + Assert.AreEqual(expectedDate, d.AsScalar.Value.AsDouble, 0.001); + + // 2. Test Date + Int (Days) + d2 := Call('+', [d, ValI(2)]); // Add 2 days + Assert.AreEqual(TScalar.TKind.DateTime, d2.AsScalar.Kind); + Assert.AreEqual(expectedDate + 2, d2.AsScalar.Value.AsDouble, 0.001); + + // 3. Test Date - Date (Diff in days) + diff := Call('-', [d2, d]); + Assert.AreEqual(TScalar.TKind.Float, diff.AsScalar.Kind); + Assert.AreEqual(2.0, diff.AsScalar.Value.AsDouble, 0.001); +end; + +procedure TTestMycAstRTL.RTL_DivByZero_ThrowException; +begin + // Integer div + Assert.WillRaise(procedure begin Call('div', [ValI(10), ValI(0)]); end); + // Float divide (explicit check in RTL) + Assert.WillRaise(procedure begin Call('/', [ValF(10.0), ValF(0.0)]); end); +end; + +end. diff --git a/Test/AST/Test.Myc.Ast.Scope.pas b/Test/AST/Test.Myc.Ast.Scope.pas new file mode 100644 index 0000000..0c206ee --- /dev/null +++ b/Test/AST/Test.Myc.Ast.Scope.pas @@ -0,0 +1,341 @@ +unit Test.Myc.Ast.Scope; + +interface + +uses + DUnitX.TestFramework, + System.SysUtils, + System.Generics.Collections, + Myc.Data.Value, + Myc.Data.Scalar, + Myc.Ast.Types, + Myc.Ast.Scope; + +type + [TestFixture] + TTestMycAstScope = class + private + // Helper to create a fully initialized scope with a specific set of variable names + function CreateTestScope(const VarNames: array of string; Parent: IExecutionScope = nil): IExecutionScope; + public + // --- Group 1: Static Layout & Builder (Parameterized) --- + + [Test] + [TestCase('Simple_A', 'A,0')] + [TestCase('Simple_B', 'B,1')] + [TestCase('Simple_C', 'C,2')] + procedure Builder_Define_AssignsSequentialSlots(const VarName: string; ExpectedSlot: Integer); + + [Test] + [TestCase('Find_A', 'A,0')] + [TestCase('Find_B', 'B,1')] + [TestCase('Find_Missing', 'Z,-1')] + [TestCase('Case_Sensitivity_Check', 'a,-1')] // Verifying default behavior (Case Sensitive in current impl) + procedure Builder_FindSlot_Checks(const SearchName: string; ExpectedSlot: Integer); + + // --- Group 2: Runtime Execution Scope & Growth --- + + [Test] + procedure Scope_DynamicGrowth_ResizesValuesArray; + + [Test] + procedure Scope_Define_DuplicateName_ThrowsException; + + // --- Group 3: Hierarchy & Shadowing (Parameterized Depth) --- + + [Test] + [TestCase('Depth_0', '0')] + [TestCase('Depth_1', '1')] + [TestCase('Depth_5', '5')] + procedure Scope_Resolve_WorksAtVariousDepths(Depth: Integer); + + [Test] + procedure Scope_DeepNesting_StressTest; // Corner Case: 100+ Levels + + // --- Group 4: Capture & Boxing (Closures) --- + + [Test] + procedure Scope_Capture_Identity_RepeatedCaptureReturnsSameCell; + + [Test] + procedure Scope_Capture_Of_Already_Captured_Upvalue; + + [Test] + procedure Scope_Uninitialized_Access_ReturnsVoidOrZero; + end; + +implementation + +{ TTestMycAstScope } + +function TTestMycAstScope.CreateTestScope(const VarNames: array of string; Parent: IExecutionScope): IExecutionScope; +var + parentLayout: IScopeLayout; + builder: IScopeBuilder; + layout: IScopeLayout; + descriptor: IScopeDescriptor; + types: TArray; + i: Integer; +begin + if Assigned(Parent) then + parentLayout := Parent.Descriptor.Layout + else + parentLayout := nil; + + builder := TScope.CreateBuilder(parentLayout); + for i := 0 to High(VarNames) do + builder.Define(VarNames[i]); + layout := builder.Build; + + SetLength(types, Length(VarNames)); + for i := 0 to High(types) do + types[i] := TTypes.Unknown; + + descriptor := TScope.CreateDescriptor(layout, types); + Result := TScope.CreateScope(Parent, descriptor, nil); +end; + +// ------------------------------------------------------------------------ +// Group 1: Static Layout & Builder +// ------------------------------------------------------------------------ + +procedure TTestMycAstScope.Builder_Define_AssignsSequentialSlots(const VarName: string; ExpectedSlot: Integer); +var + builder: IScopeBuilder; +begin + builder := TScope.CreateBuilder(nil); + // We define the context first + builder.Define('A'); // 0 + builder.Define('B'); // 1 + builder.Define('C'); // 2 + + // Note: Since we construct the builder fresh every time, we simulate looking up + // in a builder that has these 3 defined. + // However, `Define` returns the index. + // To test Define return value correctly based on TestCase, we'd need a switch. + // Instead, let's test FindSlot here which validates the Define logic implicitly. + + Assert.AreEqual(ExpectedSlot, builder.FindSlot(VarName)); +end; + +procedure TTestMycAstScope.Builder_FindSlot_Checks(const SearchName: string; ExpectedSlot: Integer); +var + builder: IScopeBuilder; +begin + builder := TScope.CreateBuilder(nil); + builder.Define('A'); + builder.Define('B'); + + Assert.AreEqual(ExpectedSlot, builder.FindSlot(SearchName)); +end; + +// ------------------------------------------------------------------------ +// Group 2: Runtime Execution Scope & Growth +// ------------------------------------------------------------------------ + +procedure TTestMycAstScope.Scope_DynamicGrowth_ResizesValuesArray; +var + scope: IExecutionScope; + addr: TResolvedAddress; + val: TDataValue; +begin + // Corner Case: Defining variables in a scope WITHOUT a descriptor (Dynamic Interpreter Mode) + // The internal array must grow. + scope := TScope.CreateScope(nil, nil, nil); + + // Slot 0 + scope.Define('A', 10); + + // Slot 100 (Simulation of a large jump or massive definition sequence) + // Note: TExecutionScope.Define assigns sequential slots. To test resize, we loop. + for var i := 1 to 100 do + scope.Define('V' + i.ToString, i); + + addr := scope.Resolve('V100'); + Assert.AreEqual(100, addr.SlotIndex); + + val := scope[addr]; + Assert.AreEqual(Int64(100), val.AsScalar.Value.AsInt64); +end; + +procedure TTestMycAstScope.Scope_Define_DuplicateName_ThrowsException; +var + scope: IExecutionScope; +begin + scope := TScope.CreateScope(nil, nil, nil); + scope.Define('X', 1); + + Assert.WillRaise(procedure begin scope.Define('X', 2); end); +end; + +procedure TTestMycAstScope.Scope_Uninitialized_Access_ReturnsVoidOrZero; +var + builder: IScopeBuilder; + layout: IScopeLayout; + descriptor: IScopeDescriptor; + scope: IExecutionScope; + addr: TResolvedAddress; +begin + // Static Layout defined, but value not set in Scope + builder := TScope.CreateBuilder(nil); + builder.Define('A'); // Slot 0 + layout := builder.Build; + descriptor := TScope.CreateDescriptor(layout, [TTypes.Unknown]); + + scope := TScope.CreateScope(nil, descriptor, nil); + + addr.Kind := akLocalOrParent; + addr.ScopeDepth := 0; + addr.SlotIndex := 0; + + // Accessing before writing + // TDataValue defaults to Kind=vkVoid via Initialize operator usually, + // but let's verify TExecutionScope constructor zeros memory or initializes generic array. + Assert.IsTrue(scope[addr].IsVoid, 'Uninitialized slot should be Void'); +end; + +// ------------------------------------------------------------------------ +// Group 3: Hierarchy & Shadowing +// ------------------------------------------------------------------------ + +procedure TTestMycAstScope.Scope_Resolve_WorksAtVariousDepths(Depth: Integer); +var + scopes: TArray; + i: Integer; + addr: TResolvedAddress; +begin + SetLength(scopes, Depth + 1); + + // Root defines Target + scopes[0] := CreateTestScope(['Target']); + + // Chain creation + for i := 1 to Depth do + scopes[i] := CreateTestScope([], scopes[i - 1]); + + // Resolve from the deepest scope + addr := scopes[Depth].Resolve('Target'); + + Assert.AreEqual(TAddressKind.akLocalOrParent, addr.Kind); + Assert.AreEqual(Depth, addr.ScopeDepth); + Assert.AreEqual(0, addr.SlotIndex); +end; + +procedure TTestMycAstScope.Scope_DeepNesting_StressTest; +const + MAX_DEPTH = 100; +var + scopes: array[0..MAX_DEPTH] of IExecutionScope; + i: Integer; + addr: TResolvedAddress; + val: TDataValue; +begin + // Corner Case: Deep Recursion / Stack Limits on Resolution + + // 1. Create Root Scope with 'DeepVar' defined in the Layout + scopes[0] := CreateTestScope(['DeepVar']); + + // 2. Set the value for the EXISTING variable. + // Do NOT call Define() again, as it would try to create a duplicate slot. + addr := scopes[0].Resolve('DeepVar'); + Assert.AreEqual(TAddressKind.akLocalOrParent, addr.Kind, 'DeepVar must be resolvable in root'); + scopes[0][addr] := 999; + + // 3. Build deep nesting chain + for i := 1 to MAX_DEPTH do + scopes[i] := CreateTestScope([], scopes[i - 1]); + + // 4. Resolve from the deepest leaf + // This forces the Resolve() method to traverse 100 parent pointers. + addr := scopes[MAX_DEPTH].Resolve('DeepVar'); + + Assert.AreEqual(TAddressKind.akLocalOrParent, addr.Kind); + Assert.AreEqual(MAX_DEPTH, addr.ScopeDepth, 'ScopeDepth must match nesting level'); + + // 5. Read the value through the deep link + val := scopes[MAX_DEPTH][addr]; + Assert.AreEqual(Int64(999), val.AsScalar.Value.AsInt64); +end; + +// ------------------------------------------------------------------------ +// Group 4: Capture & Boxing (Closures) - CRITICAL +// ------------------------------------------------------------------------ + +procedure TTestMycAstScope.Scope_Capture_Identity_RepeatedCaptureReturnsSameCell; +var + scope: IExecutionScope; + addr: TResolvedAddress; + cell1, cell2: IValueCell; +begin + // Corner Case: If I capture the same variable twice (e.g. two lambdas in same scope using same var), + // they MUST share the underlying storage (Box). + + scope := CreateTestScope(['A']); + addr := scope.Resolve('A'); + + cell1 := scope.Capture(addr); + cell2 := scope.Capture(addr); + + // They must be the same interface pointer instance + Assert.IsTrue(cell1 = cell2, 'Repeated capture must return identical interface instance'); + + // Modify one, check other + cell1.Value := 100; + Assert.AreEqual(Int64(100), cell2.Value.AsScalar.Value.AsInt64); +end; + +procedure TTestMycAstScope.Scope_Capture_Of_Already_Captured_Upvalue; +var + root, mid, leaf: IExecutionScope; + rootAddr: TResolvedAddress; + midAddr: TResolvedAddress; // Points to root's var as an upvalue + leafAddr: TResolvedAddress; + cellRoot: IValueCell; + capturedInMid: TArray; +begin + // Scenario: + // Root: [VarX] + // Mid: (Captures VarX from Root) -> This creates a closure context for Mid + // Leaf: (Captures VarX from Mid) -> This captures the *already captured* cell + + // 1. Root + root := CreateTestScope(['VarX']); + rootAddr := root.Resolve('VarX'); + root[rootAddr] := 42; + + // 2. Mid (Simulate a Lambda Scope that captured VarX) + // To simulate this, we need to construct Mid such that it has 'capturedUpvalues' populated. + // We manually capture from root first. + cellRoot := root.Capture(rootAddr); + SetLength(capturedInMid, 1); + capturedInMid[0] := cellRoot; + + // Mid's descriptor needs to know it has an upvalue? + // No, IExecutionScope just holds the array. The address resolution handles mapping. + mid := TScope.CreateScope(root, nil, capturedInMid); + + // 3. Leaf resolves VarX. + // If Leaf is physically inside Mid, it might refer to VarX via Mid's Upvalue list. + // Address: Kind=akUpvalue, Slot=0 (index into capturedInMid) + leafAddr.Kind := akUpvalue; + leafAddr.SlotIndex := 0; + // Note: ScopeDepth irrelevant for akUpvalue access + + // 4. Test Access from Leaf via Upvalue chain + // leaf does not have its own captured array yet, but we ask it to GET the value at that address + // Actually, 'leaf' typically represents the *execution* of the lambda. + // If the lambda code says "get upvalue 0", the evaluator calls GetValues(akUpvalue, 0). + + // Let's simulate Leaf access: + leaf := TScope.CreateScope(mid, nil, capturedInMid); // Leaf shares the capture array for this test context + + Assert.AreEqual(Int64(42), leaf[leafAddr].AsScalar.Value.AsInt64); + + // 5. Modify via Leaf reference + leaf[leafAddr] := 99; + + // Check Root + Assert.AreEqual(Int64(99), root[rootAddr].AsScalar.Value.AsInt64); +end; + +end. diff --git a/Test/MycTests.dpr b/Test/MycTests.dpr index 9fc575c..af09295 100644 --- a/Test/MycTests.dpr +++ b/Test/MycTests.dpr @@ -32,9 +32,9 @@ uses Myc.Data.Decimal in '..\Src\Data\Myc.Data.Decimal.pas', TestDataDecimal in 'TestDataDecimal.pas', TestDataPOD in 'TestDataPOD.pas', - Test.Ast.Interpreter.Scope in 'Test.Ast.Interpreter.Scope.pas', Myc.Data.Chunks in '..\Src\Data\Myc.Data.Chunks.pas', - Test.Myc.Data.Value in 'Test.Myc.Data.Value.pas'; + Test.Myc.Ast.Scope in 'AST\Test.Myc.Ast.Scope.pas', + Test.Myc.Ast.RTL in 'AST\Test.Myc.Ast.RTL.pas'; { keep comment here to protect the following conditional from being removed by the IDE when adding a unit } {$IFNDEF TESTINSIGHT} diff --git a/Test/MycTests.dproj b/Test/MycTests.dproj index 0194e3f..36a7df2 100644 --- a/Test/MycTests.dproj +++ b/Test/MycTests.dproj @@ -135,9 +135,9 @@ $(PreBuildEvent)]]> - - + + Base diff --git a/Test/Test.Ast.Interpreter.Scope.pas b/Test/Test.Ast.Interpreter.Scope.pas deleted file mode 100644 index 53d6d28..0000000 --- a/Test/Test.Ast.Interpreter.Scope.pas +++ /dev/null @@ -1,295 +0,0 @@ -unit Test.Ast.Interpreter.Scope; - -interface - -uses - Myc.Ast, - Myc.Ast.Nodes, - Myc.Ast.Binding, - Myc.Ast.Evaluator, - Myc.Ast.Scope, - Myc.Data.Scalar, - Myc.Data.Value, - DUnitX.TestFramework; - -type - [TestFixture] - TInterpreterScopeTests = class(TObject) - private - FGlobalScope: IExecutionScope; - function Execute(const ANode: IAstNode): TDataValue; - public - [Setup] - procedure Setup; - [TearDown] - procedure TearDown; - - [Test] - procedure Test_DeeplyNestedLambda_ModifiesUpvalue; - - [Test] - procedure Test_SeparateClosures_ShareSameUpvalue; - - [Test] - procedure Test_NestedLambda_CapturesParameter; - - [Test] - procedure Test_VariableShadowing_DoesNotAffectUpvalue; - - [Test] - procedure Test_CaptureFromGlobalScope; - - [Test] - procedure Test_ClosureCaptureWithParentScopeReallocation; - - end; - -implementation - -{ TInterpreterScopeTests } - -function TInterpreterScopeTests.Execute(const ANode: IAstNode): TDataValue; -var - boundNode: IAstNode; - descriptor: IScopeDescriptor; - runtimeScope: IExecutionScope; - visitor: IEvaluatorVisitor; -begin - // Helper to encapsulate the Bind -> CreateScope -> Evaluate pattern. - // The binder needs an evaluator factory to expand macros (even if there are none). - boundNode := - TAstBinder.Bind( - FGlobalScope, - ANode, - descriptor, - function(const Scope: IExecutionScope): IEvaluatorVisitor begin Result := TEvaluatorVisitor.Create(Scope); end - ); - runtimeScope := descriptor.CreateScope(FGlobalScope); - visitor := TEvaluatorVisitor.Create(runtimeScope); - Result := visitor.Execute(boundNode); -end; - -procedure TInterpreterScopeTests.Setup; -begin - FGlobalScope := TAst.CreateScope(nil); -end; - -procedure TInterpreterScopeTests.TearDown; -begin - FGlobalScope := nil; -end; - -procedure TInterpreterScopeTests.Test_DeeplyNestedLambda_ModifiesUpvalue; -var - mainBlock: IAstNode; - resultValue: TDataValue; -begin - // This is our original, simple test case with three nested lambdas. - mainBlock := - TAst.Block( - [ - TAst.VarDecl( - TAst.Identifier('outer'), - TAst.LambdaExpr( - [], - TAst.Block( - [ - TAst.VarDecl(TAst.Identifier('x'), TAst.Constant(TScalar.FromInt64(10))), - TAst.VarDecl( - TAst.Identifier('inner'), - TAst.LambdaExpr( - [], - TAst.Block( - [ - TAst.VarDecl( - TAst.Identifier('innermost'), - TAst.LambdaExpr( - [], - TAst.Assign( - TAst.Identifier('x'), - TAst.BinaryExpr( - TAst.Identifier('x'), - TScalar.TBinaryOp.Add, - TAst.Constant(TScalar.FromInt64(5)) - ) - ) - ) - ), - TAst.FunctionCall(TAst.Identifier('innermost'), []) - ] - ) - ) - ), - TAst.FunctionCall(TAst.Identifier('inner'), []), - TAst.Identifier('x') - ] - ) - ) - ), - TAst.VarDecl(TAst.Identifier('finalResult'), TAst.FunctionCall(TAst.Identifier('outer'), [])) - ] - ); - - resultValue := Execute(mainBlock); - Assert.AreEqual(15, resultValue.AsScalar.Value.AsInt64, 'The final result should be 15.'); -end; - -procedure TInterpreterScopeTests.Test_SeparateClosures_ShareSameUpvalue; -var - mainBlock: IAstNode; - resultValue: TDataValue; -begin - // This is our more complex "modifier/reader" test case. - mainBlock := - TAst.Block( - [ - TAst.VarDecl(TAst.Identifier('a'), TAst.Constant(TScalar.FromInt64(10))), - TAst.VarDecl( - TAst.Identifier('modifier'), - TAst.LambdaExpr( - [], // Outer modifier shell - TAst.LambdaExpr( - [], // Inner closure that is returned - TAst.Assign( - TAst.Identifier('a'), - TAst.BinaryExpr(TAst.Identifier('a'), TScalar.TBinaryOp.Add, TAst.Constant(TScalar.FromInt64(5))) - ) - ) - ) - ), - TAst.VarDecl(TAst.Identifier('reader'), TAst.LambdaExpr([], TAst.Identifier('a'))), - TAst.VarDecl(TAst.Identifier('innermost_closure'), TAst.FunctionCall(TAst.Identifier('modifier'), [])), - TAst.FunctionCall(TAst.Identifier('innermost_closure'), []), - TAst.FunctionCall(TAst.Identifier('reader'), []) - ] - ); - - resultValue := Execute(mainBlock); - Assert.AreEqual(15, resultValue.AsScalar.Value.AsInt64, 'The final result should be 15.'); -end; - -procedure TInterpreterScopeTests.Test_NestedLambda_CapturesParameter; -var - mainBlock: IAstNode; - resultValue: TDataValue; -begin - // Tests if a nested lambda can correctly capture a PARAMETER of its parent lambda. - mainBlock := - TAst.Block( - [ - TAst.VarDecl( - TAst.Identifier('factory'), - TAst.LambdaExpr( - [TAst.Identifier('p')], // Parameter 'p' - TAst.LambdaExpr( - [], // Returned lambda captures 'p' - TAst.BinaryExpr(TAst.Identifier('p'), TScalar.TBinaryOp.Multiply, TAst.Constant(TScalar.FromInt64(2))) - ) - ) - ), - TAst.VarDecl( - TAst.Identifier('multiplier'), - TAst.FunctionCall(TAst.Identifier('factory'), [TAst.Constant(TScalar.FromInt64(21))]) - ), - TAst.FunctionCall(TAst.Identifier('multiplier'), []) - ] - ); - - resultValue := Execute(mainBlock); - Assert.AreEqual(42, resultValue.AsScalar.Value.AsInt64, 'The result of 21 * 2 should be 42.'); -end; - -procedure TInterpreterScopeTests.Test_VariableShadowing_DoesNotAffectUpvalue; -var - mainBlock: IAstNode; - resultValue: TDataValue; -begin - // Tests that a local variable 'x' correctly "shadows" a parent's variable 'x'. - // The modification of the inner 'x' must not affect the outer 'x'. - mainBlock := - TAst.Block( - [ - TAst.VarDecl(TAst.Identifier('x'), TAst.Constant(TScalar.FromInt64(10))), - TAst.FunctionCall( - TAst.LambdaExpr( - [], - TAst.Block( - [ - // This 'x' should shadow the outer 'x'. - TAst.VarDecl(TAst.Identifier('x'), TAst.Constant(TScalar.FromInt64(50))), - TAst.Assign(TAst.Identifier('x'), TAst.Constant(TScalar.FromInt64(99))) - ] - ) - ), - [] - ), - // This final expression should return the value of the original, outer 'x'. - TAst.Identifier('x') - ] - ); - - resultValue := Execute(mainBlock); - Assert.AreEqual(10, resultValue.AsScalar.Value.AsInt64, 'The outer "x" should remain unchanged.'); -end; - -procedure TInterpreterScopeTests.Test_CaptureFromGlobalScope; -var - mainBlock: IAstNode; - resultValue: TDataValue; -begin - // Defines a variable in the global scope and ensures a simple script can access it. - FGlobalScope.Define('g', TScalar.FromInt64(99)); - mainBlock := TAst.BinaryExpr(TAst.Identifier('g'), TScalar.TBinaryOp.Add, TAst.Constant(TScalar.FromInt64(1))); - - resultValue := Execute(mainBlock); - Assert.AreEqual(100, resultValue.AsScalar.Value.AsInt64, 'Should be able to access variables from the parent scope.'); -end; - -procedure TInterpreterScopeTests.Test_ClosureCaptureWithParentScopeReallocation; -var - rootScope: IExecutionScope; - parentScope: IExecutionScope; - lambdaScope: IExecutionScope; - addressOfX_from_lambda: TResolvedAddress; - addressOfX_in_parent: TResolvedAddress; - capturedCell: IValueCell; - valueFromClosure: TDataValue; -begin - // 1. Setup scopes: root -> parent -> lambda - rootScope := TScope.CreateScope(nil, nil, nil); - parentScope := TScope.CreateScope(rootScope, nil, nil); - lambdaScope := TScope.CreateScope(parentScope, nil, nil); - - // 2. Define a variable 'x' in the parent scope. It will be at slot 0. - parentScope.Define('x', 10); - addressOfX_in_parent := TResolvedAddress.Create(akLocalOrParent, 0, 0); - Assert.AreEqual(Int64(10), parentScope[addressOfX_in_parent].AsScalar.Value.AsInt64); - - // 3. From the lambda's perspective, 'x' is one level up (ScopeDepth=1) at slot 0. - addressOfX_from_lambda := TResolvedAddress.Create(akLocalOrParent, 1, 0); - - // 4. Capture 'x' into a value cell, simulating a closure. - // This creates the buggy TValueRef that holds a direct reference to the parent's internal array. - capturedCell := lambdaScope.Capture(addressOfX_from_lambda); - Assert.AreEqual(Int64(10), capturedCell.Value.AsScalar.Value.AsInt64, 'Initial captured value should be correct'); - - // 5. Trigger the bug: Define another variable in the parent scope. - // This forces a SetLength on the internal FValues array, which may cause a reallocation. - parentScope.Define('y', 20); - - // 6. Update the original variable 'x' in the parent scope to a new value. - parentScope[addressOfX_in_parent] := 99; - Assert.AreEqual(Int64(99), parentScope[addressOfX_in_parent].AsScalar.Value.AsInt64, 'Value in parent scope should be updated'); - - // 7. Read the value from the captured cell again. - // The test will fail here. The captured cell still points to the old, orphaned memory block - // where the value of x is still 10, not the new value 99. - valueFromClosure := capturedCell.Value; - Assert.AreEqual( - Int64(99), - valueFromClosure.AsScalar.Value.AsInt64, - 'The captured cell must reflect changes in the parent scope after reallocation' - ); -end; - -end. diff --git a/Test/Test.Myc.Data.Value.pas b/Test/Test.Myc.Data.Value.pas deleted file mode 100644 index 781bcec..0000000 --- a/Test/Test.Myc.Data.Value.pas +++ /dev/null @@ -1,173 +0,0 @@ -unit Test.Myc.Data.Value; - -interface - -uses - DUnitX.TestFramework, - System.SysUtils, - System.Threading, - Myc.Data.Value, - Myc.Data.Scalar; - -type - [TestFixture] - TTestDataValue = class(TObject) - private - type - // Simple object for interface tests - TTestObject = class(TInterfacedObject, IInterface) - public - ID: Integer; - end; - public - [Test] - procedure TestReset_Scalar; - [Test] - procedure TestReset_Interface; - [Test] - procedure TestCompareAndSet_Scalar; - [Test] - procedure TestCompareAndSet_Interface; - [Test] - procedure TestKindImmutability_ThrowsException; - [Test] - procedure TestAtomicity_ConcurrentIncrement; - end; - -implementation - -{ TTestDataValue } - -procedure TTestDataValue.TestReset_Scalar; -var - val: TDataValue; - oldVal: TDataValue; -begin - // Test with Int64 - val := 10; - oldVal := val.Reset(20); - Assert.AreEqual(Int64(20), val.AsScalar.Value.AsInt64, 'Value should be updated to 20'); - Assert.AreEqual(Int64(10), oldVal.AsScalar.Value.AsInt64, 'Reset should return the old value 10'); - - // Test with Double - val := 10.5; - oldVal := val.Reset(20.5); - Assert.AreEqual(Double(20.5), val.AsScalar.Value.AsDouble, 'Value should be updated to 20.5'); - Assert.AreEqual(Double(10.5), oldVal.AsScalar.Value.AsDouble, 'Reset should return the old value 10.5'); -end; - -procedure TTestDataValue.TestReset_Interface; -var - objA, objB: IInterface; - val: TDataValue; - oldVal: TDataValue; -begin - objA := TTestObject.Create; - objB := TTestObject.Create; - - val := TDataValue.FromIntf(objA); - oldVal := val.Reset(TDataValue.FromIntf(objB)); - - Assert.AreSame(objB, val.AsIntf, 'Value should be updated to objB'); - Assert.AreSame(objA, oldVal.AsIntf, 'Reset should return the old value objA'); -end; - -procedure TTestDataValue.TestCompareAndSet_Scalar; -var - val: TDataValue; -begin - val := 100; - - // Successful CAS - Assert.IsTrue(val.CompareAndSet(100, 200), 'CAS should succeed when expected value matches'); - Assert.AreEqual(Int64(200), val.AsScalar.Value.AsInt64, 'Value should be 200 after successful CAS'); - - // Failing CAS - Assert.IsFalse(val.CompareAndSet(100, 300), 'CAS should fail when expected value does not match'); - Assert.AreEqual(Int64(200), val.AsScalar.Value.AsInt64, 'Value should remain 200 after failed CAS'); -end; - -procedure TTestDataValue.TestCompareAndSet_Interface; -var - objA, objB, objC: IInterface; - val: TDataValue; -begin - objA := TTestObject.Create; - objB := TTestObject.Create; - objC := TTestObject.Create; - val := TDataValue.FromIntf(objA); - - // Successful CAS - Assert.IsTrue(val.CompareAndSet(TDataValue.FromIntf(objA), TDataValue.FromIntf(objB)), 'CAS should succeed for interfaces'); - Assert.AreSame(objB, val.AsIntf, 'Value should be objB after successful CAS'); - - // Failing CAS - Assert.IsFalse(val.CompareAndSet(TDataValue.FromIntf(objA), TDataValue.FromIntf(objC)), 'CAS should fail for interfaces'); - Assert.AreSame(objB, val.AsIntf, 'Value should remain objB after failed CAS'); -end; - -procedure TTestDataValue.TestKindImmutability_ThrowsException; -var - scalarVal, textVal: TDataValue; -begin - scalarVal := 10; - textVal := 'hello'; - - // Test Reset - Assert.WillRaise(procedure begin scalarVal.Reset(textVal); end, EArgumentException, 'Reset must throw exception on kind mismatch'); - - // Test CompareAndSet - Assert.WillRaise( - procedure - begin - // NewValue has wrong kind - scalarVal.CompareAndSet(10, textVal); - end, - EArgumentException, - 'CompareAndSet must throw exception on kind mismatch' - ); - - // Expected value doesn't match kind, should just fail silently (return False) - Assert.IsFalse(scalarVal.CompareAndSet(textVal, 20), 'CAS should return False if kinds of self and expected differ'); -end; - -procedure TTestDataValue.TestAtomicity_ConcurrentIncrement; -const - NumThreads = 8; - IncrementsPerThread = 25000; -var - sharedValue: TDataValue; - tasks: array of ITask; - i: Integer; -begin - sharedValue := 0; // TDataValue of kind vkScalar, Ordinal - SetLength(tasks, NumThreads); - - for i := 0 to High(tasks) do - begin - tasks[i] := - TTask.Run( - procedure - var - j: Integer; - oldVal, newVal: TDataValue; - begin - for j := 1 to IncrementsPerThread do - begin - // This is the classic lock-free swap loop - repeat - oldVal := sharedValue; // Read current value - newVal := oldVal.AsScalar.Value.AsInt64 + 1; - until sharedValue.CompareAndSet(oldVal, newVal); - end; - end - ); - end; - - TTask.WaitForAll(tasks); - - const Expected = NumThreads * IncrementsPerThread; - Assert.AreEqual(Int64(Expected), sharedValue.AsScalar.Value.AsInt64, 'Concurrent increments should result in the correct total sum'); -end; - -end. diff --git a/Test/TestDataPOD.pas b/Test/TestDataPOD.pas index 682573d..e208e62 100644 --- a/Test/TestDataPOD.pas +++ b/Test/TestDataPOD.pas @@ -28,8 +28,6 @@ type procedure TestScalarArray; [Test] procedure TestScalarTuple; - [Test] - procedure TestScalarRecordAndDefinition; end; implementation @@ -80,39 +78,6 @@ begin Assert.AreEqual(Int64(0), Length(arr.Items), 'Empty array length should be zero'); end; -procedure TTestPOD.TestScalarRecordAndDefinition; -var - recDef: TScalarRecordDefinition; - values: TArray; - rec: TScalarRecord; - mismatchedValues: TArray; -begin - // 1. Create a definition - recDef := - TScalarRecordDefinition - .Create([TScalarRecordField.Create('ID', TScalar.TKind.Ordinal), TScalarRecordField.Create('Price', TScalar.TKind.Float)]); - Assert.AreEqual(Int64(2), Length(recDef.Fields), 'Record definition field count mismatch'); - Assert.AreEqual('Price', recDef.Fields[1].Name, 'Record definition field name mismatch'); - - // 2. Create a matching set of values - values := [TScalar.FromInt64(99).Value, TScalar.FromDouble(19.95).Value]; - - // 3. Create the record - rec := TScalarRecord.Create(recDef, values); - Assert.AreEqual(Int64(2), Length(rec.Fields), 'Record field count mismatch'); - Assert.AreEqual(19.95, rec.Fields[1].AsDouble, 1E-12, 'Record field value mismatch'); - Assert.AreEqual(Int64(99), rec.Items['ID'].Value.AsInt64, 'Record item by name mismatch'); - Assert.AreEqual(TScalar.TKind.Float, rec.Items['Price'].Kind, 'Record item kind by name mismatch'); - - // 4. Test assertion for mismatched field count - mismatchedValues := [TScalar.FromInt64(1).Value]; - Assert.WillRaise( - procedure begin rec := TScalarRecord.Create(recDef, mismatchedValues); end, - EAssertionFailed, - 'Mismatched field/value count should raise an assertion' - ); -end; - procedure TTestPOD.TestScalarTuple; var tuple: TScalarTuple;