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] [IgnoreMemoryLeaks] 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.