Optional types and null propagation

This commit is contained in:
Michael Schimmel
2025-12-21 15:20:59 +01:00
parent e7fdbc3312
commit ac96a105a5
10 changed files with 840 additions and 190 deletions
+157
View File
@@ -0,0 +1,157 @@
unit Test.Myc.Ast.NullPropagation;
interface
uses
DUnitX.TestFramework,
System.SysUtils,
System.Classes,
Myc.Data.Value,
Myc.Data.Scalar,
Myc.Ast.Environment,
Myc.Ast.Nodes, // For exception types
Myc.Ast.Script; // WICHTIG: Für Parsing
type
[TestFixture]
TTestNullPropagation = class
private
FEnv: TAstEnvironment;
public
[Setup]
procedure Setup;
[TearDown]
procedure TearDown;
[Test]
[TestCase('ValidRecord', 'true,10')]
[TestCase('VoidRecord', 'false,0')]
procedure TestMemberAccessPropagation(const Condition: Boolean; const ExpectedVal: Int64);
[Test]
procedure TestNestedPropagation;
[Test]
procedure TestIndexerPropagation;
[Test]
procedure TestStrictArithmeticFailure;
[Test]
procedure TestStrictComparisonFailure;
end;
implementation
{ TTestNullPropagation }
procedure TTestNullPropagation.Setup;
begin
// Create a fresh standard environment for each test
FEnv := TAstEnvironment.Construct(nil);
end;
procedure TTestNullPropagation.TearDown;
begin
FEnv := Default(TAstEnvironment);
end;
procedure TTestNullPropagation.TestMemberAccessPropagation(const Condition: Boolean; const ExpectedVal: Int64);
var
script: string;
resultVal: TDataValue;
begin
// Script:
// 1. Define 'r' which is EITHER a Record {:a 10} OR Void depending on condition.
// 2. Access field .a -> Should propagate Void if r is Void.
script := Format('(do ' + ' (def r (if %s {:a 10}))' + ' (.a r)' + ')', [BoolToStr(Condition, True).ToLower]);
resultVal := FEnv.Run(TAstScript.Parse(script));
if Condition then
begin
// Case: Object exists
Assert.AreEqual(TDataValueKind.vkScalar, resultVal.Kind, 'Should return a scalar');
Assert.AreEqual(ExpectedVal, resultVal.AsScalar.Value.AsInt64, 'Should return the field value');
end
else
begin
// Case: Object is Void (Null Propagation)
Assert.IsTrue(resultVal.IsVoid, 'Accessing member of Void should yield Void');
end;
end;
procedure TTestNullPropagation.TestNestedPropagation;
var
script: string;
resultVal: TDataValue;
begin
// Chained Access: (get (.data (if false ...)) 0)
script := '(do ' + ' (def root (if false {:data (new-series "ordinal")}))' + ' (get (.data root) 0)' + ')';
resultVal := FEnv.Run(TAstScript.Parse(script));
Assert.IsTrue(resultVal.IsVoid, 'Nested access on Void root should propagate Void all the way up');
end;
procedure TTestNullPropagation.TestIndexerPropagation;
var
script: string;
resultVal: TDataValue;
begin
// 1. Define 's' as Optional<Series>
// 2. Access via Indexer
script := '(do ' + ' (def s (if false (new-series "ordinal")))' + ' (get s 42)' + ')';
// FIX: Parse String to AST Node
resultVal := FEnv.Run(TAstScript.Parse(script));
Assert.IsTrue(resultVal.IsVoid, 'Indexer access on Void series should yield Void');
end;
procedure TTestNullPropagation.TestStrictArithmeticFailure;
begin
// Requirement: Implicit Lifting is FORBIDDEN for arithmetic.
Assert.WillRaise(
procedure
begin
FEnv.Run(
TAstScript.Parse(
'''
(do
(def a (if true 10)) ; a is Optional<Ordinal>
(+ a 5) ; Error: Operator + not defined for Optional
)
'''
)
);
end,
ECompilationFailed,
'Arithmetic on optional types must trigger a compilation error'
);
end;
procedure TTestNullPropagation.TestStrictComparisonFailure;
begin
// Requirement: Comparisons with Optional types are FORBIDDEN.
Assert.WillRaise(
procedure
begin
FEnv.Run(
TAstScript.Parse(
'(do '
+ ' (def a (if true 10))'
+ // a is Optional<Ordinal>
' (> a 5)'
+ // Error
')'
)
);
end,
ECompilationFailed,
'Comparison on optional types must trigger a compilation error'
);
end;
end.
+2 -1
View File
@@ -33,7 +33,8 @@ uses
Test.Myc.Ast.Compiler.Macros in 'AST\Test.Myc.Ast.Compiler.Macros.pas',
Test.Myc.Ast.RTL.DateTime in 'AST\Test.Myc.Ast.RTL.DateTime.pas',
Test.Myc.Ast.Compiler.Binder in '..\Src\AST\Test.Myc.Ast.Compiler.Binder.pas',
Test.Myc.Ast.RTL.TypeRegistry in 'AST\Test.Myc.Ast.RTL.TypeRegistry.pas';
Test.Myc.Ast.RTL.TypeRegistry in 'AST\Test.Myc.Ast.RTL.TypeRegistry.pas',
Test.Myc.Ast.NullPropagation in 'AST\Test.Myc.Ast.NullPropagation.pas';
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
{$IFNDEF TESTINSIGHT}
+1
View File
@@ -135,6 +135,7 @@ $(PreBuildEvent)]]></PreBuildEvent>
<DCCReference Include="AST\Test.Myc.Ast.RTL.DateTime.pas"/>
<DCCReference Include="..\Src\AST\Test.Myc.Ast.Compiler.Binder.pas"/>
<DCCReference Include="AST\Test.Myc.Ast.RTL.TypeRegistry.pas"/>
<DCCReference Include="AST\Test.Myc.Ast.NullPropagation.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>