[wasm][debugger] Support deep member accesses for EvaluteOnCallFrame (#40836)
authorAnkit Jain <radical@gmail.com>
Tue, 18 Aug 2020 02:13:35 +0000 (22:13 -0400)
committerGitHub <noreply@github.com>
Tue, 18 Aug 2020 02:13:35 +0000 (22:13 -0400)
* [wasm][debugger] Support deep member accesses for EvaluteOnCallFrame

Eg. `obj.Property.X`, or `obj.Y + obj.Z.p`

Each of the member access expressions (like `a.b.c`) must be of only
primitive types. Though if the expression is a single member access (and
nothing else, like `"a.b.c"`), then that can be a non-primitive type.

This works by sending the expression to the browser, where it gets
resolved by `library_mono.js`. And that takes an easy route for doing
this, essentially just fetching the list of locals/properties, and using
that.

There are better ways to do this, that will be explored later.

* [wasm][debugger][tests] Remove some debug spew

* [wasm][debugger] fix formatting with dotnet-format

src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs
src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs
src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs
src/mono/wasm/debugger/DebuggerTestSuite/Support.cs
src/mono/wasm/debugger/tests/debugger-evaluate-test.cs
src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs
src/mono/wasm/debugger/tests/other.js
src/mono/wasm/runtime/library_mono.js

index a08a625..9a5761f 100644 (file)
@@ -193,6 +193,12 @@ namespace Microsoft.WebAssembly.Diagnostics
             return new MonoCommands($"MONO.mono_wasm_get_variables({scopeId}, {JsonConvert.SerializeObject(var_ids)})");
         }
 
+        public static MonoCommands EvaluateMemberAccess(int scopeId, string expr, params VarInfo[] vars)
+        {
+            var var_ids = vars.Select(v => new { index = v.Index, name = v.Name }).ToArray();
+            return new MonoCommands($"MONO.mono_wasm_eval_member_access({scopeId}, {JsonConvert.SerializeObject(var_ids)}, '', '{expr}')");
+        }
+
         public static MonoCommands SetBreakpoint(string assemblyName, uint methodToken, int ilOffset) => new MonoCommands($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})");
 
         public static MonoCommands RemoveBreakpoint(int breakpointId) => new MonoCommands($"MONO.mono_wasm_remove_breakpoint({breakpointId})");
@@ -285,7 +291,7 @@ namespace Microsoft.WebAssembly.Diagnostics
         internal DebugStore store;
         public TaskCompletionSource<DebugStore> Source { get; } = new TaskCompletionSource<DebugStore>();
 
-        public Dictionary<string, JToken> LocalsCache = new Dictionary<string, JToken>();
+        Dictionary<int, PerScopeCache> perScopeCaches { get; } = new Dictionary<int, PerScopeCache>();
 
         public DebugStore Store
         {
@@ -298,11 +304,26 @@ namespace Microsoft.WebAssembly.Diagnostics
             }
         }
 
+        public PerScopeCache GetCacheForScope(int scope_id)
+        {
+            if (perScopeCaches.TryGetValue(scope_id, out var cache))
+                return cache;
+
+            cache = new PerScopeCache();
+            perScopeCaches[scope_id] = cache;
+            return cache;
+        }
+
         public void ClearState()
         {
             CallStack = null;
-            LocalsCache.Clear();
+            perScopeCaches.Clear();
         }
+    }
 
+    internal class PerScopeCache
+    {
+        public Dictionary<string, JObject> Locals { get; } = new Dictionary<string, JObject>();
+        public Dictionary<string, JObject> MemberReferences { get; } = new Dictionary<string, JObject>();
     }
 }
index 9f34078..8d43bda 100644 (file)
@@ -6,12 +6,14 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
 using Microsoft.CodeAnalysis.Emit;
+using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 
 namespace Microsoft.WebAssembly.Diagnostics
@@ -19,54 +21,30 @@ namespace Microsoft.WebAssembly.Diagnostics
 
     internal class EvaluateExpression
     {
-
-        class FindThisExpression : CSharpSyntaxWalker
+        class FindVariableNMethodCall : CSharpSyntaxWalker
         {
-            public List<string> thisExpressions = new List<string>();
-            public SyntaxTree syntaxTree;
-            public FindThisExpression(SyntaxTree syntax)
-            {
-                syntaxTree = syntax;
-            }
+            public List<IdentifierNameSyntax> identifiers = new List<IdentifierNameSyntax>();
+            public List<InvocationExpressionSyntax> methodCall = new List<InvocationExpressionSyntax>();
+            public List<MemberAccessExpressionSyntax> memberAccesses = new List<MemberAccessExpressionSyntax>();
+            public List<object> argValues = new List<Object>();
+
             public override void Visit(SyntaxNode node)
             {
-                if (node is ThisExpressionSyntax)
+                // TODO: PointerMemberAccessExpression
+                if (node is MemberAccessExpressionSyntax maes
+                    && node.Kind() == SyntaxKind.SimpleMemberAccessExpression
+                    && !(node.Parent is MemberAccessExpressionSyntax))
                 {
-                    if (node.Parent is MemberAccessExpressionSyntax thisParent && thisParent.Name is IdentifierNameSyntax)
-                    {
-                        IdentifierNameSyntax var = thisParent.Name as IdentifierNameSyntax;
-                        thisExpressions.Add(var.Identifier.Text);
-                        var newRoot = syntaxTree.GetRoot().ReplaceNode(node.Parent, thisParent.Name);
-                        syntaxTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);
-                        this.Visit(GetExpressionFromSyntaxTree(syntaxTree));
-                    }
+                    memberAccesses.Add(maes);
                 }
-                else
-                    base.Visit(node);
-            }
 
-            public async Task CheckIfIsProperty(MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
-            {
-                foreach (var var in thisExpressions)
+                if (node is IdentifierNameSyntax identifier
+                    && !(identifier.Parent is MemberAccessExpressionSyntax)
+                    && !identifiers.Any(x => x.Identifier.Text == identifier.Identifier.Text))
                 {
-                    JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var, true, token);
-                    if (value == null)
-                        throw new Exception($"The property {var} does not exist in the current context");
+                    identifiers.Add(identifier);
                 }
-            }
-        }
-
-        class FindVariableNMethodCall : CSharpSyntaxWalker
-        {
-            public List<IdentifierNameSyntax> variables = new List<IdentifierNameSyntax>();
-            public List<ThisExpressionSyntax> thisList = new List<ThisExpressionSyntax>();
-            public List<InvocationExpressionSyntax> methodCall = new List<InvocationExpressionSyntax>();
-            public List<object> values = new List<Object>();
 
-            public override void Visit(SyntaxNode node)
-            {
-                if (node is IdentifierNameSyntax identifier && !variables.Any(x => x.Identifier.Text == identifier.Identifier.Text))
-                    variables.Add(identifier);
                 if (node is InvocationExpressionSyntax)
                 {
                     methodCall.Add(node as InvocationExpressionSyntax);
@@ -76,29 +54,74 @@ namespace Microsoft.WebAssembly.Diagnostics
                     throw new Exception("Assignment is not implemented yet");
                 base.Visit(node);
             }
-            public async Task<SyntaxTree> ReplaceVars(SyntaxTree syntaxTree, MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
+
+            public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_values, IEnumerable<JObject> id_values)
             {
                 CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot();
-                foreach (var var in variables)
+                var memberAccessToParamName = new Dictionary<string, string>();
+
+                // 1. Replace all this.a occurrences with this_a_ABDE
+                root = root.ReplaceNodes(memberAccesses, (maes, _) =>
+                {
+                    var ma_str = maes.ToString();
+                    if (!memberAccessToParamName.TryGetValue(ma_str, out var id_name))
+                    {
+                        // Generate a random suffix
+                        string suffix = Guid.NewGuid().ToString().Substring(0, 5);
+                        string prefix = ma_str.Trim().Replace(".", "_");
+                        id_name = $"{prefix}_{suffix}";
+
+                        memberAccessToParamName[ma_str] = id_name;
+                    }
+
+                    return SyntaxFactory.IdentifierName(id_name);
+                });
+
+                var paramsSet = new HashSet<string>();
+
+                // 2. For every unique member ref, add a corresponding method param
+                foreach (var (maes, value) in memberAccesses.Zip(ma_values))
                 {
-                    ClassDeclarationSyntax classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax;
-                    MethodDeclarationSyntax method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax;
+                    var node_str = maes.ToString();
+                    if (!memberAccessToParamName.TryGetValue(node_str, out var id_name))
+                    {
+                        throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}");
+                    }
 
-                    JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var.Identifier.Text, false, token);
+                    root = UpdateWithNewMethodParam(root, id_name, value);
+                }
 
-                    if (value == null)
-                        throw new Exception($"The name {var.Identifier.Text} does not exist in the current context");
+                foreach (var (idns, value) in identifiers.Zip(id_values))
+                {
+                    root = UpdateWithNewMethodParam(root, idns.Identifier.Text, value);
+                }
 
-                    values.Add(ConvertJSToCSharpType(value["value"]));
+                return syntaxTree.WithRootAndOptions(root, syntaxTree.Options);
+
+                CompilationUnitSyntax UpdateWithNewMethodParam(CompilationUnitSyntax root, string id_name, JObject value)
+                {
+                    var classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax;
+                    var method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax;
+
+                    if (paramsSet.Contains(id_name))
+                    {
+                        // repeated member access expression
+                        // eg. this.a + this.a
+                        return root;
+                    }
+
+                    argValues.Add(ConvertJSToCSharpType(value));
 
                     var updatedMethod = method.AddParameterListParameters(
                         SyntaxFactory.Parameter(
-                            SyntaxFactory.Identifier(var.Identifier.Text))
-                        .WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value["value"]))));
+                            SyntaxFactory.Identifier(id_name))
+                            .WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value))));
+
+                    paramsSet.Add(id_name);
                     root = root.ReplaceNode(method, updatedMethod);
+
+                    return root;
                 }
-                syntaxTree = syntaxTree.WithRootAndOptions(root, syntaxTree.Options);
-                return syntaxTree;
             }
 
             private object ConvertJSToCSharpType(JToken variable)
@@ -120,7 +143,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                             return null;
                         break;
                 }
-                throw new Exception($"Evaluate of this datatype {type} not implemented yet");
+                throw new Exception($"Evaluate of this datatype {type} not implemented yet");//, "Unsupported");
             }
 
             private string GetTypeFullName(JToken variable)
@@ -140,7 +163,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                     default:
                         return value.GetType().FullName;
                 }
-                throw new Exception($"Evaluate of this datatype {type} not implemented yet");
+                throw new ReturnAsErrorException($"GetTypefullName: Evaluate of this datatype {type} not implemented yet", "Unsupported");
             }
         }
 
@@ -151,36 +174,89 @@ namespace Microsoft.WebAssembly.Diagnostics
             MethodDeclarationSyntax methodDeclaration = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax;
             BlockSyntax blockValue = methodDeclaration.Body;
             ReturnStatementSyntax returnValue = blockValue.Statements.ElementAt(0) as ReturnStatementSyntax;
-            InvocationExpressionSyntax expressionInvocation = returnValue.Expression as InvocationExpressionSyntax;
-            MemberAccessExpressionSyntax expressionMember = expressionInvocation.Expression as MemberAccessExpressionSyntax;
-            ParenthesizedExpressionSyntax expressionParenthesized = expressionMember.Expression as ParenthesizedExpressionSyntax;
-            return expressionParenthesized.Expression;
+            ParenthesizedExpressionSyntax expressionParenthesized = returnValue.Expression as ParenthesizedExpressionSyntax;
+
+            return expressionParenthesized?.Expression;
         }
 
-        internal static async Task<string> CompileAndRunTheExpression(MonoProxy proxy, MessageId msg_id, int scope_id, string expression, CancellationToken token)
+        private static async Task<IList<JObject>> ResolveMemberAccessExpressions(IEnumerable<MemberAccessExpressionSyntax> member_accesses,
+                                MemberReferenceResolver resolver, CancellationToken token)
+        {
+            var memberAccessValues = new List<JObject>();
+            foreach (var maes in member_accesses)
+            {
+                var memberAccessString = maes.ToString();
+                var value = await resolver.Resolve(memberAccessString, token);
+                if (value == null)
+                    throw new ReturnAsErrorException($"Failed to resolve member access for {memberAccessString}", "ReferenceError");
+
+                memberAccessValues.Add(value);
+            }
+
+            return memberAccessValues;
+        }
+
+        private static async Task<IList<JObject>> ResolveIdentifiers(IEnumerable<IdentifierNameSyntax> identifiers, MemberReferenceResolver resolver, CancellationToken token)
+        {
+            var values = new List<JObject>();
+            foreach (var var in identifiers)
+            {
+                JObject value = await resolver.Resolve(var.Identifier.Text, token);
+                if (value == null)
+                    throw new ReturnAsErrorException($"The name {var.Identifier.Text} does not exist in the current context", "ReferenceError");
+
+                values.Add(value);
+            }
+
+            return values;
+        }
+
+        internal static async Task<JObject> CompileAndRunTheExpression(string expression, MemberReferenceResolver resolver, CancellationToken token)
         {
-            FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall();
-            string retString;
             SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
-                               using System;
-                               public class CompileAndRunTheExpression
-                               {
-                                       public string Evaluate()
-                                       {
-                                               return (" + expression + @").ToString(); 
-                                       }
-                               }");
-
-            FindThisExpression findThisExpression = new FindThisExpression(syntaxTree);
+                using System;
+                public class CompileAndRunTheExpression
+                {
+                    public static object Evaluate()
+                    {
+                        return (" + expression + @");
+                    }
+                }");
+
             var expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
-            findThisExpression.Visit(expressionTree);
-            await findThisExpression.CheckIfIsProperty(proxy, msg_id, scope_id, token);
-            syntaxTree = findThisExpression.syntaxTree;
+            if (expressionTree == null)
+                throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree");
 
-            expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
+            FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall();
             findVarNMethodCall.Visit(expressionTree);
 
-            syntaxTree = await findVarNMethodCall.ReplaceVars(syntaxTree, proxy, msg_id, scope_id, token);
+            // this fails with `"a)"`
+            // because the code becomes: return (a));
+            // and the returned expression from GetExpressionFromSyntaxTree is `a`!
+            if (expressionTree.Kind() == SyntaxKind.IdentifierName || expressionTree.Kind() == SyntaxKind.ThisExpression)
+            {
+                var var_name = expressionTree.ToString();
+                var value = await resolver.Resolve(var_name, token);
+                if (value == null)
+                    throw new ReturnAsErrorException($"Cannot find member named '{var_name}'.", "ReferenceError");
+
+                return value;
+            }
+
+            var memberAccessValues = await ResolveMemberAccessExpressions(findVarNMethodCall.memberAccesses, resolver, token);
+
+            // this.dateTime
+            if (expressionTree.Kind() == SyntaxKind.SimpleMemberAccessExpression && findVarNMethodCall.memberAccesses.Count == 1)
+            {
+                return memberAccessValues[0]?["value"]?.Value<JObject>();
+            }
+
+            var identifierValues = await ResolveIdentifiers(findVarNMethodCall.identifiers, resolver, token);
+
+            syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, memberAccessValues, identifierValues);
+            expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
+            if (expressionTree == null)
+                throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree");
 
             MetadataReference[] references = new MetadataReference[]
             {
@@ -193,21 +269,83 @@ namespace Microsoft.WebAssembly.Diagnostics
                 syntaxTrees: new[] { syntaxTree },
                 references: references,
                 options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+            var semanticModel = compilation.GetSemanticModel(syntaxTree);
+            var typeInfo = semanticModel.GetTypeInfo(expressionTree);
+
             using (var ms = new MemoryStream())
             {
                 EmitResult result = compilation.Emit(ms);
+                if (!result.Success)
+                {
+                    var sb = new StringBuilder();
+                    foreach (var d in result.Diagnostics)
+                        sb.Append(d.ToString());
+
+                    throw new ReturnAsErrorException(sb.ToString(), "CompilationError");
+                }
+
                 ms.Seek(0, SeekOrigin.Begin);
                 Assembly assembly = Assembly.Load(ms.ToArray());
                 Type type = assembly.GetType("CompileAndRunTheExpression");
-                object obj = Activator.CreateInstance(type);
+
                 var ret = type.InvokeMember("Evaluate",
-                    BindingFlags.Default | BindingFlags.InvokeMethod,
+                    BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
+                    null,
                     null,
-                    obj,
-                    findVarNMethodCall.values.ToArray());
-                retString = ret.ToString();
+                    findVarNMethodCall.argValues.ToArray());
+
+                return JObject.FromObject(ConvertCSharpToJSType(ret, typeInfo.Type));
+            }
+        }
+
+        static readonly HashSet<Type> NumericTypes = new HashSet<Type>
+        {
+            typeof(decimal), typeof(byte), typeof(sbyte),
+            typeof(short), typeof(ushort),
+            typeof(int), typeof(uint),
+            typeof(float), typeof(double)
+        };
+
+        static object ConvertCSharpToJSType(object v, ITypeSymbol type)
+        {
+            if (v == null)
+                return new { type = "object", subtype = "null", className = type.ToString() };
+
+            if (v is string s)
+            {
+                return new { type = "string", value = s, description = s };
+            }
+            else if (NumericTypes.Contains(v.GetType()))
+            {
+                return new { type = "number", value = v, description = v.ToString() };
             }
-            return retString;
+            else
+            {
+                return new { type = "object", value = v, description = v.ToString(), className = type.ToString() };
+            }
+        }
+
+    }
+
+    class ReturnAsErrorException : Exception
+    {
+        public Result Error { get; }
+        public ReturnAsErrorException(JObject error)
+            => Error = Result.Err(error);
+
+        public ReturnAsErrorException(string message, string className)
+        {
+            Error = Result.Err(JObject.FromObject(new
+            {
+                result = new
+                {
+                    type = "object",
+                    subtype = "error",
+                    description = message,
+                    className
+                }
+            }));
         }
     }
 }
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs
new file mode 100644 (file)
index 0000000..145e223
--- /dev/null
@@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+    internal class MemberReferenceResolver
+    {
+        private MessageId messageId;
+        private int scopeId;
+        private MonoProxy proxy;
+        private ExecutionContext ctx;
+        private PerScopeCache scopeCache;
+        private VarInfo[] varIds;
+        private ILogger logger;
+        private bool locals_fetched = false;
+
+        public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, MessageId msg_id, int scope_id, ILogger logger)
+        {
+            messageId = msg_id;
+            scopeId = scope_id;
+            this.proxy = proxy;
+            this.ctx = ctx;
+            this.logger = logger;
+            scopeCache = ctx.GetCacheForScope(scope_id);
+        }
+
+        // Checks Locals, followed by `this`
+        public async Task<JObject> Resolve(string var_name, CancellationToken token)
+        {
+            if (scopeCache.Locals.Count == 0 && !locals_fetched)
+            {
+                var scope_res = await proxy.GetScopeProperties(messageId, scopeId, token);
+                if (scope_res.IsErr)
+                    throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}");
+                locals_fetched = true;
+            }
+
+            if (scopeCache.Locals.TryGetValue(var_name, out var obj))
+            {
+                return obj["value"]?.Value<JObject>();
+            }
+
+            if (scopeCache.MemberReferences.TryGetValue(var_name, out var ret))
+                return ret;
+
+            if (varIds == null)
+            {
+                var scope = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId);
+                varIds = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
+            }
+
+            var res = await proxy.SendMonoCommand(messageId, MonoCommands.EvaluateMemberAccess(scopeId, var_name, varIds), token);
+            if (res.IsOk)
+            {
+                ret = res.Value?["result"]?["value"]?["value"]?.Value<JObject>();
+                scopeCache.MemberReferences[var_name] = ret;
+            }
+            else
+            {
+                logger.LogDebug(res.Error.ToString());
+            }
+
+            return ret;
+        }
+
+    }
+}
index 4166c7e..afd7d96 100644 (file)
@@ -426,7 +426,9 @@ namespace Microsoft.WebAssembly.Diagnostics
         async Task<Result> RuntimeGetProperties(MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token)
         {
             if (objectId.Scheme == "scope")
+            {
                 return await GetScopeProperties(id, int.Parse(objectId.Value), token);
+            }
 
             var res = await SendMonoCommand(id, MonoCommands.GetDetails(objectId, args), token);
             if (res.IsErr)
@@ -673,64 +675,6 @@ namespace Microsoft.WebAssembly.Diagnostics
             return true;
         }
 
-        internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj)
-        {
-            if (ctx.LocalsCache.TryGetValue(expression, out obj))
-            {
-                if (only_search_on_this && obj["fromThis"] == null)
-                    return false;
-                return true;
-            }
-            return false;
-        }
-
-        internal async Task<JToken> TryGetVariableValue(MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token)
-        {
-            JToken thisValue = null;
-            var context = GetContext(msg_id);
-            if (context.CallStack == null)
-                return null;
-
-            if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj))
-                return obj;
-
-            var scope = context.CallStack.FirstOrDefault(s => s.Id == scope_id);
-            var live_vars = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
-            //get_this
-            var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, live_vars), token);
-
-            var scope_values = res.Value?["result"]?["value"]?.Values<JObject>()?.ToArray();
-            thisValue = scope_values?.FirstOrDefault(v => v["name"]?.Value<string>() == "this");
-
-            if (!only_search_on_this)
-            {
-                if (thisValue != null && expression == "this")
-                    return thisValue;
-
-                var value = scope_values.SingleOrDefault(sv => sv["name"]?.Value<string>() == expression);
-                if (value != null)
-                    return value;
-            }
-
-            //search in scope
-            if (thisValue != null)
-            {
-                if (!DotnetObjectId.TryParse(thisValue["value"]["objectId"], out var objectId))
-                    return null;
-
-                res = await SendMonoCommand(msg_id, MonoCommands.GetDetails(objectId), token);
-                scope_values = res.Value?["result"]?["value"]?.Values<JObject>().ToArray();
-                var foundValue = scope_values.FirstOrDefault(v => v["name"].Value<string>() == expression);
-                if (foundValue != null)
-                {
-                    foundValue["fromThis"] = true;
-                    context.LocalsCache[foundValue["name"].Value<string>()] = foundValue;
-                    return foundValue;
-                }
-            }
-            return null;
-        }
-
         async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token)
         {
             try
@@ -739,35 +683,40 @@ namespace Microsoft.WebAssembly.Diagnostics
                 if (context.CallStack == null)
                     return false;
 
-                var varValue = await TryGetVariableValue(msg_id, scope_id, expression, false, token);
+                var resolver = new MemberReferenceResolver(this, context, msg_id, scope_id, logger);
 
-                if (varValue != null)
+                JObject retValue = await resolver.Resolve(expression, token);
+                if (retValue == null)
+                {
+                    retValue = await EvaluateExpression.CompileAndRunTheExpression(expression, resolver, token);
+                }
+
+                if (retValue != null)
                 {
                     SendResponse(msg_id, Result.OkFromObject(new
                     {
-                        result = varValue["value"]
+                        result = retValue
                     }), token);
-                    return true;
                 }
-
-                string retValue = await EvaluateExpression.CompileAndRunTheExpression(this, msg_id, scope_id, expression, token);
-                SendResponse(msg_id, Result.OkFromObject(new
+                else
                 {
-                    result = new
-                    {
-                        value = retValue
-                    }
-                }), token);
-                return true;
+                    SendResponse(msg_id, Result.Err($"Unable to evaluate {expression}"), token);
+                }
+            }
+            catch (ReturnAsErrorException ree)
+            {
+                SendResponse(msg_id, ree.Error, token);
             }
             catch (Exception e)
             {
-                logger.LogDebug(e, $"Error in EvaluateOnCallFrame for expression '{expression}.");
+                logger.LogDebug($"Error in EvaluateOnCallFrame for expression '{expression}' with '{e}.");
+                SendResponse(msg_id, Result.Exception(e), token);
             }
-            return false;
+
+            return true;
         }
 
-        async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token)
+        internal async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token)
         {
             try
             {
@@ -788,8 +737,11 @@ namespace Microsoft.WebAssembly.Diagnostics
                 if (values == null || values.Length == 0)
                     return Result.OkFromObject(new { result = Array.Empty<object>() });
 
+                var frameCache = ctx.GetCacheForScope(scope_id);
                 foreach (var value in values)
-                    ctx.LocalsCache[value["name"]?.Value<string>()] = value;
+                {
+                    frameCache.Locals[value["name"]?.Value<string>()] = value;
+                }
 
                 return Result.OkFromObject(new { result = values });
             }
index dad28df..8721810 100644 (file)
@@ -2,6 +2,8 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Threading.Tasks;
 using Microsoft.WebAssembly.Diagnostics;
@@ -10,205 +12,446 @@ using Xunit;
 
 namespace DebuggerTests
 {
-
+    // TODO: static async, static method args
     public class EvaluateOnCallFrameTests : DebuggerTestBase
     {
+        public static IEnumerable<object[]> InstanceMethodsTestData(string type_name)
+        {
+            yield return new object[] { type_name, "InstanceMethod", "InstanceMethod", false };
+            yield return new object[] { type_name, "GenericInstanceMethod", "GenericInstanceMethod<int>", false };
+            yield return new object[] { type_name, "InstanceMethodAsync", "MoveNext", true };
+            yield return new object[] { type_name, "GenericInstanceMethodAsync", "MoveNext", true };
 
-        [Fact]
-        public async Task EvaluateThisProperties() => await CheckInspectLocalsAtBreakpointSite(
-            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
-            "run",
-            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+            // TODO: { "DebuggerTests.EvaluateTestsGeneric`1", "Instance", 9, "EvaluateTestsGenericStructInstanceMethod", prefix }
+        }
+
+        public static IEnumerable<object[]> InstanceMethodForTypeMembersTestData(string type_name)
+        {
+            foreach (var data in InstanceMethodsTestData(type_name))
+            {
+                yield return new object[] { "", 0 }.Concat(data).ToArray();
+                yield return new object[] { "this.", 0 }.Concat(data).ToArray();
+                yield return new object[] { "NewInstance.", 3 }.Concat(data).ToArray();
+                yield return new object[] { "this.NewInstance.", 3 }.Concat(data).ToArray();
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")]
+        [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")]
+        public async Task EvaluateTypeInstanceMembers(string prefix, int bias, string type, string method, string bp_function_name, bool is_async)
+        => await CheckInspectLocalsAtBreakpointSite(
+            type, method, /*line_offset*/1, bp_function_name,
+            $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);",
             wait_for_event_fn: async (pause_location) =>
            {
-               var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
-               var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a");
-               CheckContentValue(evaluate, "1");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "b");
-               CheckContentValue(evaluate, "2");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "c");
-               CheckContentValue(evaluate, "3");
-
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "dt");
-               await CheckDateTimeValue(evaluate, new DateTime(2000, 5, 4, 3, 2, 1));
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+               var dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias);
+               var DTProp = dateTime.AddMinutes(10);
+
+               await EvaluateOnCallFrameAndCheck(id,
+                   (prefix + "a", TNumber(4)),
+
+                   // fields
+                   (prefix + "dateTime.TimeOfDay", TValueType("System.TimeSpan", dateTime.TimeOfDay.ToString())),
+                   (prefix + "dateTime", TDateTime(dateTime)),
+                   (prefix + "dateTime.TimeOfDay.Minutes", TNumber(dateTime.TimeOfDay.Minutes)),
+
+                   // properties
+                   (prefix + "DTProp.TimeOfDay.Minutes", TNumber(DTProp.TimeOfDay.Minutes)),
+                   (prefix + "DTProp", TDateTime(DTProp)),
+                   (prefix + "DTProp.TimeOfDay", TValueType("System.TimeSpan", DTProp.TimeOfDay.ToString())),
+
+                   (prefix + "IntProp", TNumber(9)),
+                   (prefix + "NullIfAIsNotZero", TObject("DebuggerTests.EvaluateTestsClassWithProperties", is_null: true))
+               );
            });
 
         [Theory]
-        [InlineData(63, 12, "EvaluateTestsStructInstanceMethod")]
-        [InlineData(79, 12, "GenericInstanceMethodOnStruct<int>")]
-        [InlineData(102, 12, "EvaluateTestsGenericStructInstanceMethod")]
-        public async Task EvaluateThisPropertiesOnStruct(int line, int col, string method_name) => await CheckInspectLocalsAtBreakpointSite(
-            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", line, col,
-            method_name,
-            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+        [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")]
+        [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")]
+        public async Task EvaluateInstanceMethodArguments(string type, string method, string bp_function_name, bool is_async)
+        => await CheckInspectLocalsAtBreakpointSite(
+            type, method, /*line_offset*/1, bp_function_name,
+            $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);",
             wait_for_event_fn: async (pause_location) =>
            {
-               var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a");
-               CheckContentValue(evaluate, "1");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "b");
-               CheckContentValue(evaluate, "2");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "c");
-               CheckContentValue(evaluate, "3");
-
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "dateTime");
-               await CheckDateTimeValue(evaluate, new DateTime(2020, 1, 2, 3, 4, 5));
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+               var DTProp = new DateTime(2010, 9, 8, 7, 6, 5).AddMinutes(10);
+               await EvaluateOnCallFrameAndCheck(id,
+                   ("g", TNumber(400)),
+                   ("h", TNumber(123)),
+                   ("valString", TString("just a test")),
+                   ("me", TObject(type)),
+
+                   // property on method arg
+                   ("me.DTProp", TDateTime(DTProp)),
+                   ("me.DTProp.TimeOfDay.Minutes", TNumber(DTProp.TimeOfDay.Minutes)),
+                   ("me.DTProp.Second + (me.IntProp - 5)", TNumber(DTProp.Second + 4)));
+           });
+
+        [Theory]
+        [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")]
+        [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")]
+        public async Task EvaluateMethodLocals(string type, string method, string bp_function_name, bool is_async)
+        => await CheckInspectLocalsAtBreakpointSite(
+            type, method, /*line_offset*/5, bp_function_name,
+            $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);",
+            wait_for_event_fn: async (pause_location) =>
+           {
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+               var dt = new DateTime(2025, 3, 5, 7, 9, 11);
+               await EvaluateOnCallFrameAndCheck(id,
+                   ("d", TNumber(401)),
+                   ("e", TNumber(402)),
+                   ("f", TNumber(403)),
+
+                   // property on a local
+                   ("local_dt", TDateTime(dt)),
+                   ("local_dt.Date", TDateTime(dt.Date)));
            });
 
         [Fact]
-        public async Task EvaluateParameters() => await CheckInspectLocalsAtBreakpointSite(
-            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
-            "run",
-            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+        public async Task EvaluateStaticLocalsWithDeepMemberAccess() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals",
+            "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
             wait_for_event_fn: async (pause_location) =>
            {
-               var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
-               var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "g");
-               CheckContentValue(evaluate, "100");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "h");
-               CheckContentValue(evaluate, "200");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "valString");
-               CheckContentValue(evaluate, "test");
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+               var dt = new DateTime(2020, 1, 2, 3, 4, 5);
+               await EvaluateOnCallFrameAndCheck(id,
+                   ("f_s.c", TNumber(4)),
+                   ("f_s", TValueType("DebuggerTests.EvaluateTestsStructWithProperties")),
+
+                   ("f_s.dateTime", TDateTime(dt)),
+                   ("f_s.dateTime.Date", TDateTime(dt.Date)));
            });
 
         [Fact]
-        public async Task EvaluateLocals() => await CheckInspectLocalsAtBreakpointSite(
-            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
-            "run",
-            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+        public async Task EvaluateLocalsAsync() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.Point", "AsyncInstanceMethod", 1, "MoveNext",
+            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })",
             wait_for_event_fn: async (pause_location) =>
            {
-               var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
-               var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "d");
-               CheckContentValue(evaluate, "101");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "e");
-               CheckContentValue(evaluate, "102");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "f");
-               CheckContentValue(evaluate, "103");
-
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_dt");
-               await CheckDateTimeValue(evaluate, new DateTime(2010, 9, 8, 7, 6, 5));
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+                // sc_arg
+                {
+                   var (sc_arg, _) = await EvaluateOnCallFrame(id, "sc_arg");
+                   await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), nameof(sc_arg));
+
+                    // Check that we did get the correct object
+                    var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value<string>());
+                   await CheckProps(sc_arg_props, new
+                   {
+                       X = TNumber(10),
+                       Y = TNumber(45),
+                       Id = TString("sc#Id"),
+                       Color = TEnum("DebuggerTests.RGB", "Blue"),
+                       PointWithCustomGetter = TGetter("PointWithCustomGetter")
+                   }, "sc_arg_props#1");
+
+                   await EvaluateOnCallFrameAndCheck(id,
+                       ("(sc_arg.PointWithCustomGetter.X)", TNumber(100)),
+                       ("sc_arg.Id + \"_foo\"", TString($"sc#Id_foo")),
+                       ("sc_arg.Id + (sc_arg.X==10 ? \"_is_ten\" : \"_not_ten\")", TString($"sc#Id_is_ten")));
+               }
+
+                // local_gs
+                {
+                   var (local_gs, _) = await EvaluateOnCallFrame(id, "local_gs");
+                   await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct<int>"), nameof(local_gs));
+
+                   var local_gs_props = await GetProperties(local_gs["objectId"]?.Value<string>());
+                   await CheckProps(local_gs_props, new
+                   {
+                       Id = TObject("string", is_null: true),
+                       Color = TEnum("DebuggerTests.RGB", "Red"),
+                       Value = TNumber(0)
+                   }, "local_gs_props#1");
+                   await EvaluateOnCallFrameAndCheck(id, ("(local_gs.Id)", TString(null)));
+               }
+           });
+
+        [Theory]
+        [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")]
+        [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")]
+        public async Task EvaluateExpressionsWithDeepMemberAccesses(string prefix, int bias, string type, string method, string bp_function_name, bool _)
+        => await CheckInspectLocalsAtBreakpointSite(
+            type, method, /*line_offset*/4, bp_function_name,
+            $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);",
+            wait_for_event_fn: async (pause_location) =>
+           {
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+               var dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias);
+               var DTProp = dateTime.AddMinutes(10);
+
+               await EvaluateOnCallFrameAndCheck(id,
+                   ($"{prefix}a + 5", TNumber(9)),
+                   ($"10 + {prefix}IntProp", TNumber(19)),
+                   ($" {prefix}IntProp  +  {prefix}DTProp.Second", TNumber(9 + DTProp.Second)),
+                   ($" {prefix}IntProp + ({prefix}DTProp.Second+{prefix}dateTime.Year)", TNumber(9 + DTProp.Second + dateTime.Year)),
+                   ($" {prefix}DTProp.Second > 0 ? \"_foo_\": \"_zero_\"", TString("_foo_")),
+
+                   // local_dt is not live yet
+                   ($"local_dt.Date.Year * 10", TNumber(10)));
            });
 
         [Fact]
-        public async Task EvaluateLocalsAsync()
+        public async Task EvaluateSimpleExpressions() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.EvaluateTestsClass/TestEvaluate", "run", 9, "run",
+            "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+            wait_for_event_fn: async (pause_location) =>
+           {
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+               await EvaluateOnCallFrameAndCheck(id,
+                   // "((this))", TObject("foo")); //FIXME:
+                   // "((dt))", TObject("foo")); //FIXME:
+
+                   ("this", TObject("DebuggerTests.EvaluateTestsClass.TestEvaluate")),
+
+                   ("5", TNumber(5)),
+                   ("d + e", TNumber(203)),
+                   ("e + 10", TNumber(112)),
+
+                   // repeated expressions
+                   ("this.a + this.a", TNumber(2)),
+                   ("a + \"_\" + a", TString("9000_9000")),
+                   ("a+(a  )", TString("90009000")),
+
+                   // possible duplicate arg name
+                   ("this.a + this_a", TNumber(46)),
+
+                   ("this.a + this.b", TNumber(3)),
+                   ("\"test\" + \"test\"", TString("testtest")),
+                   ("5 + 5", TNumber(10)));
+           });
+
+        public static TheoryData<string, string, string> ShadowMethodArgsTestData => new TheoryData<string, string, string>
         {
-            var bp_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
-            int line = 249;
-            int col = 12;
-            var function_name = "MoveNext";
-            await CheckInspectLocalsAtBreakpointSite(
-                bp_loc, line, col,
-                function_name,
-                "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })",
-                wait_for_event_fn: async (pause_location) =>
+            { "DebuggerTests.EvaluateTestsClassWithProperties", "EvaluateShadow", "EvaluateShadow" },
+            { "DebuggerTests.EvaluateTestsClassWithProperties", "EvaluateShadowAsync", "MoveNext" },
+            { "DebuggerTests.EvaluateTestsStructWithProperties", "EvaluateShadow", "EvaluateShadow" },
+            { "DebuggerTests.EvaluateTestsStructWithProperties", "EvaluateShadowAsync", "MoveNext" },
+        };
+
+        [Theory]
+        [MemberData(nameof(ShadowMethodArgsTestData))]
+        public async Task LocalsAndArgsShadowingThisMembers(string type_name, string method, string bp_function_name) => await CheckInspectLocalsAtBreakpointSite(
+            type_name, method, 2, bp_function_name,
+            "window.setTimeout(function() { invoke_static_method ('[debugger-test] " + type_name + ":run'); })",
+            wait_for_event_fn: async (pause_location) =>
+           {
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+               await EvaluateOnCallFrameAndCheck(id,
+                   ("a", TString("hello")),
+                   ("this.a", TNumber(4)));
+
+               await CheckExpressions("this.", new DateTime(2010, 9, 8, 7, 6, 5 + 0));
+               await CheckExpressions(String.Empty, new DateTime(2020, 3, 4, 5, 6, 7));
+
+               async Task CheckExpressions(string prefix, DateTime dateTime)
                {
-                   var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                   await EvaluateOnCallFrameAndCheck(id,
+                       (prefix + "dateTime", TDateTime(dateTime)),
+                       (prefix + "dateTime.TimeOfDay.Minutes", TNumber(dateTime.TimeOfDay.Minutes)),
+                       (prefix + "dateTime.TimeOfDay", TValueType("System.TimeSpan", dateTime.TimeOfDay.ToString())));
+               }
+           });
 
-                   // sc_arg
-                   {
-                       var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "sc_arg");
-                       await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#1");
-
-                       var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value<string>());
-                       await CheckProps(sc_arg_props, new
-                       {
-                           X = TNumber(10),
-                           Y = TNumber(45),
-                           Id = TString("sc#Id"),
-                           Color = TEnum("DebuggerTests.RGB", "Blue"),
-                           PointWithCustomGetter = TGetter("PointWithCustomGetter")
-                       }, "sc_arg_props#1");
-                   }
-
-                   // local_gs
-                   {
-                       var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_gs");
-                       await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct<int>"), "local_gs#1");
-
-                       var local_gs_props = await GetProperties(local_gs["objectId"]?.Value<string>());
-                       await CheckProps(local_gs_props, new
-                       {
-                           Id = TObject("string", is_null: true),
-                           Color = TEnum("DebuggerTests.RGB", "Red"),
-                           Value = TNumber(0)
-                       }, "local_gs_props#1");
-                   }
-
-                   // step, check local_gs
-                   pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 1, col, function_name);
-                   {
-                       var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_gs");
-                       await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct<int>"), "local_gs#2");
-
-                       var local_gs_props = await GetProperties(local_gs["objectId"]?.Value<string>());
-                       await CheckProps(local_gs_props, new
-                       {
-                           Id = TString("local_gs#Id"),
-                           Color = TEnum("DebuggerTests.RGB", "Green"),
-                           Value = TNumber(4)
-                       }, "local_gs_props#2");
-                   }
-
-                   // step check sc_arg.Id
-                   pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 2, col, function_name);
-                   {
-                       var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "sc_arg");
-                       await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#2");
-
-                       var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value<string>());
-                       await CheckProps(sc_arg_props, new
-                       {
-                           X = TNumber(10),
-                           Y = TNumber(45),
-                           Id = TString("sc_arg#Id"), // <------- This changed
-                           Color = TEnum("DebuggerTests.RGB", "Blue"),
-                           PointWithCustomGetter = TGetter("PointWithCustomGetter")
-                       }, "sc_arg_props#2");
-                   }
-               });
+        [Theory]
+        [InlineData("DebuggerTests.EvaluateTestsStructWithProperties", true)]
+        [InlineData("DebuggerTests.EvaluateTestsClassWithProperties", false)]
+        public async Task EvaluateOnPreviousFrames(string type_name, bool is_valuetype) => await CheckInspectLocalsAtBreakpointSite(
+            type_name, "EvaluateShadow", 1, "EvaluateShadow",
+            $"window.setTimeout(function() {{ invoke_static_method ('[debugger-test] {type_name}:run'); }})",
+            wait_for_event_fn: async (pause_location) =>
+           {
+               var dt_local = new DateTime(2020, 3, 4, 5, 6, 7);
+               var dt_this = new DateTime(2010, 9, 8, 7, 6, 5);
+
+                // At EvaluateShadow
+                {
+                   var id0 = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+                   await EvaluateOnCallFrameAndCheck(id0,
+                       ("dateTime", TDateTime(dt_local)),
+                       ("this.dateTime", TDateTime(dt_this))
+                   );
+
+                   await EvaluateOnCallFrameFail(id0, ("obj.IntProp", "ReferenceError"));
+               }
+
+               {
+                   var id1 = pause_location["callFrames"][1]["callFrameId"].Value<string>();
+                   await EvaluateOnCallFrameFail(id1,
+                       ("dateTime", "ReferenceError"),
+                       ("this.dateTime", "ReferenceError"));
+
+                    // obj available only on the -1 frame
+                    await EvaluateOnCallFrameAndCheck(id1, ("obj.IntProp", TNumber(7)));
+               }
+
+               await SetBreakpointInMethod("debugger-test.dll", type_name, "SomeMethod", 1);
+               pause_location = await SendCommandAndCheck(null, "Debugger.resume", null, 0, 0, "SomeMethod");
+
+                // At SomeMethod
+
+                // TODO: change types also.. so, that `this` is different!
+
+                // Check frame0
+                {
+                   var id0 = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+                    // 'me' and 'dateTime' are reversed in this method
+                    await EvaluateOnCallFrameAndCheck(id0,
+                       ("dateTime", is_valuetype ? TValueType(type_name) : TObject(type_name)),
+                       ("this.dateTime", TDateTime(dt_this)),
+                       ("me", TDateTime(dt_local)),
+
+                       // local variable shadows field, but isn't "live" yet
+                       ("DTProp", TString(null)),
+
+                       // access field via `this.`
+                       ("this.DTProp", TDateTime(dt_this.AddMinutes(10))));
+
+                   await EvaluateOnCallFrameFail(id0, ("obj", "ReferenceError"));
+               }
+
+                // check frame1
+                {
+                   var id1 = pause_location["callFrames"][1]["callFrameId"].Value<string>();
+
+                   await EvaluateOnCallFrameAndCheck(id1,
+                       // 'me' and 'dateTime' are reversed in this method
+                       ("dateTime", TDateTime(dt_local)),
+                       ("this.dateTime", TDateTime(dt_this)),
+                       ("me", is_valuetype ? TValueType(type_name) : TObject(type_name)),
+
+                       // not shadowed here
+                       ("DTProp", TDateTime(dt_this.AddMinutes(10))),
+
+                       // access field via `this.`
+                       ("this.DTProp", TDateTime(dt_this.AddMinutes(10))));
+
+                   await EvaluateOnCallFrameFail(id1, ("obj", "ReferenceError"));
+               }
+
+                // check frame2
+                {
+                   var id2 = pause_location["callFrames"][2]["callFrameId"].Value<string>();
+
+                    // Only obj should be available
+                    await EvaluateOnCallFrameFail(id2,
+                       ("dateTime", "ReferenceError"),
+                       ("this.dateTime", "ReferenceError"),
+                       ("me", "ReferenceError"));
+
+                   await EvaluateOnCallFrameAndCheck(id2, ("obj", is_valuetype ? TValueType(type_name) : TObject(type_name)));
+               }
+           });
+
+        [Fact]
+        public async Task JSEvaluate()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            var bp_loc = "/other.js";
+            var line = 76;
+            var col = 1;
+
+            await Ready();
+            await insp.Ready(async (cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                await SetBreakpoint(bp_loc, line, col);
+
+                var eval_expr = "window.setTimeout(function() { eval_call_on_frame_test (); }, 1)";
+                var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token);
+                var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+                var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+                await EvaluateOnCallFrameFail(id,
+                    ("me.foo", null),
+                    ("obj.foo.bar", null));
+
+                await EvaluateOnCallFrame(id, "obj.foo", expect_ok: true);
+            });
         }
 
         [Fact]
-        public async Task EvaluateExpressions() => await CheckInspectLocalsAtBreakpointSite(
-            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
-            "run",
-            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+        public async Task NegativeTestsInInstanceMethod() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.EvaluateTestsClass/TestEvaluate", "run", 9, "run",
+            "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
             wait_for_event_fn: async (pause_location) =>
            {
-               var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
-               var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "d + e");
-               CheckContentValue(evaluate, "203");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "e + 10");
-               CheckContentValue(evaluate, "112");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a + a");
-               CheckContentValue(evaluate, "2");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.a + this.b");
-               CheckContentValue(evaluate, "3");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "\"test\" + \"test\"");
-               CheckContentValue(evaluate, "testtest");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "5 + 5");
-               CheckContentValue(evaluate, "10");
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+                // Use '.' on a primitive member
+                await EvaluateOnCallFrameFail(id,
+                   //BUG: TODO:
+                   //("a)", "CompilationError"),
+
+                   ("this.a.", "ReferenceError"),
+                   ("a.", "ReferenceError"),
+
+                   ("this..a", "CompilationError"),
+                   (".a.", "ReferenceError"),
+
+                   ("me.foo", "ReferenceError"),
+
+                   ("this.a + non_existant", "ReferenceError"),
+
+                   ("this.NullIfAIsNotZero.foo", "ReferenceError"),
+                   ("NullIfAIsNotZero.foo", "ReferenceError"));
            });
 
         [Fact]
-        public async Task EvaluateThisExpressions() => await CheckInspectLocalsAtBreakpointSite(
-            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
-            "run",
-            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+        public async Task NegativeTestsInStaticMethod() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals",
+            "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
             wait_for_event_fn: async (pause_location) =>
            {
-               var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
-               var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.a");
-               CheckContentValue(evaluate, "1");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.b");
-               CheckContentValue(evaluate, "2");
-               evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.c");
-               CheckContentValue(evaluate, "3");
-
-               // FIXME: not supported yet
-               // evaluate = await EvaluateOnCallFrame (pause_location ["callFrames"][0] ["callFrameId"].Value<string> (), "this.dt");
-               // await CheckDateTimeValue (evaluate, new DateTime (2000, 5, 4, 3, 2, 1));
+               var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+               await EvaluateOnCallFrameFail(id,
+                   ("me.foo", "ReferenceError"),
+                   ("this", "ReferenceError"),
+                   ("this.NullIfAIsNotZero.foo", "ReferenceError"));
            });
+
+        async Task EvaluateOnCallFrameAndCheck(string call_frame_id, params (string expression, JObject expected)[] args)
+        {
+            foreach (var arg in args)
+            {
+                var (eval_val, _) = await EvaluateOnCallFrame(call_frame_id, arg.expression);
+                try
+                {
+                    await CheckValue(eval_val, arg.expected, arg.expression);
+                }
+                catch
+                {
+                    Console.WriteLine($"CheckValue failed for {arg.expression}. Expected: {arg.expected}, vs {eval_val}");
+                    throw;
+                }
+            }
+        }
+
+        async Task EvaluateOnCallFrameFail(string call_frame_id, params (string expression, string class_name)[] args)
+        {
+            foreach (var arg in args)
+            {
+                var (_, res) = await EvaluateOnCallFrame(call_frame_id, arg.expression, expect_ok: false);
+                if (arg.class_name != null)
+                    AssertEqual(arg.class_name, res.Error["result"]?["className"]?.Value<string>(), $"Error className did not match for expression '{arg.expression}'");
+            }
+        }
     }
 
 }
index d7df335..81501e4 100644 (file)
@@ -14,6 +14,7 @@ using Microsoft.WebAssembly.Diagnostics;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 using Xunit;
+using Xunit.Sdk;
 
 namespace DebuggerTests
 {
@@ -338,6 +339,12 @@ namespace DebuggerTests
             return l;
         }
 
+        internal async Task CheckDateTime(JToken value, DateTime expected, string label = "")
+        {
+            await CheckValue(value, TValueType("System.DateTime", expected.ToString()), label);
+            await CheckDateTimeValue(value, expected);
+        }
+
         internal async Task CheckDateTime(JToken locals, string name, DateTime expected)
         {
             var obj = GetAndAssertObjectWithName(locals, name);
@@ -616,6 +623,13 @@ namespace DebuggerTests
                         break;
                     }
 
+                case "datetime":
+                    {
+                        var dateTime = DateTime.FromBinary(exp_val["binary"].Value<long>());
+                        await CheckDateTime(actual_val, dateTime, label);
+                        break;
+                    }
+
                 case "ignore_me":
                     // nothing to check ;)
                     break;
@@ -822,7 +836,7 @@ namespace DebuggerTests
             return locals;
         }
 
-        internal async Task<JToken> EvaluateOnCallFrame(string id, string expression)
+        internal async Task<(JToken, Result)> EvaluateOnCallFrame(string id, string expression, bool expect_ok = true)
         {
             var evaluate_req = JObject.FromObject(new
             {
@@ -830,12 +844,12 @@ namespace DebuggerTests
                 expression = expression
             });
 
-            var frame_evaluate = await ctx.cli.SendCommand("Debugger.evaluateOnCallFrame", evaluate_req, ctx.token);
-            if (!frame_evaluate.IsOk)
-                Assert.True(false, $"Debugger.evaluateOnCallFrame failed for {evaluate_req.ToString()}, with Result: {frame_evaluate}");
+            var res = await ctx.cli.SendCommand("Debugger.evaluateOnCallFrame", evaluate_req, ctx.token);
+            AssertEqual(expect_ok, res.IsOk, $"Debugger.evaluateOnCallFrame ('{expression}', scope: {id}) returned {res.IsOk} instead of {expect_ok}, with Result: {res}");
+            if (res.IsOk)
+                return (res.Value["result"], res);
 
-            var evaluate_result = frame_evaluate.Value["result"];
-            return evaluate_result;
+            return (null, res);
         }
 
         internal async Task<Result> SetBreakpoint(string url_key, int line, int column, bool expect_ok = true, bool use_regex = false)
@@ -880,10 +894,15 @@ namespace DebuggerTests
             return res;
         }
 
-        internal void AssertEqual(object expected, object actual, string label) => Assert.True(expected?.Equals(actual),
-            $"[{label}]\n" +
-            $"Expected: {expected?.ToString()}\n" +
-            $"Actual:   {actual?.ToString()}\n");
+        internal void AssertEqual(object expected, object actual, string label)
+        {
+            if (expected?.Equals(actual) == true)
+                return;
+
+            throw new AssertActualExpectedException(
+                expected, actual,
+                $"[{label}]\n");
+        }
 
         internal void AssertStartsWith(string expected, string actual, string label) => Assert.True(actual?.StartsWith(expected), $"[{label}] Does not start with the expected string\nExpected: {expected}\nActual:   {actual}");
 
@@ -907,7 +926,7 @@ namespace DebuggerTests
         //FIXME: um maybe we don't need to convert jobject right here!
         internal static JObject TString(string value) =>
             value == null ?
-            TObject("string", is_null: true) :
+            JObject.FromObject(new { type = "object", className = "string", subtype = "null" }) :
             JObject.FromObject(new { type = "string", value = @value });
 
         internal static JObject TNumber(int value) =>
@@ -951,6 +970,12 @@ namespace DebuggerTests
         internal static JObject TIgnore() => JObject.FromObject(new { __custom_type = "ignore_me" });
 
         internal static JObject TGetter(string type) => JObject.FromObject(new { __custom_type = "getter", type_name = type });
+
+        internal static JObject TDateTime(DateTime dt) => JObject.FromObject(new
+        {
+            __custom_type = "datetime",
+            binary = dt.ToBinary()
+        });
     }
 
     class DebugTestContext
index ffb6fad..498c028 100644 (file)
@@ -13,17 +13,18 @@ namespace DebuggerTests
             public int b;
             public int c;
             public DateTime dt = new DateTime(2000, 5, 4, 3, 2, 1);
-            public void run(int g, int h, string valString)
+            public TestEvaluate NullIfAIsNotZero => a != 0 ? null : this;
+            public void run(int g, int h, string a, string valString, int this_a)
             {
                 int d = g + 1;
                 int e = g + 2;
                 int f = g + 3;
                 int i = d + e + f;
                 var local_dt = new DateTime(2010, 9, 8, 7, 6, 5);
-                a = 1;
+                this.a = 1;
                 b = 2;
                 c = 3;
-                a = a + 1;
+                this.a = this.a + 1;
                 b = b + 1;
                 c = c + 1;
             }
@@ -32,31 +33,103 @@ namespace DebuggerTests
         public static void EvaluateLocals()
         {
             TestEvaluate f = new TestEvaluate();
-            f.run(100, 200, "test");
+            f.run(100, 200, "9000", "test", 45);
 
-            var f_s = new EvaluateTestsStruct();
-            f_s.EvaluateTestsStructInstanceMethod(100, 200, "test");
-            f_s.GenericInstanceMethodOnStruct<int>(100, 200, "test");
+            var f_s = new EvaluateTestsStructWithProperties();
+            f_s.InstanceMethod(100, 200, "test", f_s);
+            f_s.GenericInstanceMethod<int>(100, 200, "test", f_s);
 
             var f_g_s = new EvaluateTestsGenericStruct<int>();
             f_g_s.EvaluateTestsGenericStructInstanceMethod(100, 200, "test");
-            Console.WriteLine($"a: {f.a}, b: {f.b}, c: {f.c}");
         }
 
     }
 
-    public struct EvaluateTestsStruct
+    public struct EvaluateTestsGenericStruct<T>
     {
         public int a;
         public int b;
         public int c;
         DateTime dateTime;
-        public void EvaluateTestsStructInstanceMethod(int g, int h, string valString)
+        public void EvaluateTestsGenericStructInstanceMethod(int g, int h, string valString)
         {
             int d = g + 1;
             int e = g + 2;
             int f = g + 3;
-            int i = d + e + f;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            T t = default(T);
+            a = a + 1;
+            b = b + 2;
+            c = c + 3;
+        }
+    }
+
+    public class EvaluateTestsClassWithProperties
+    {
+        public int a;
+        public int b;
+        public int c { get; set; }
+
+        public DateTime dateTime;
+        public DateTime DTProp => dateTime.AddMinutes(10);
+        public int IntProp => a + 5;
+        public string SetOnlyProp { set { a = value.Length; } }
+        public EvaluateTestsClassWithProperties NullIfAIsNotZero => a != 1908712 ? null : new EvaluateTestsClassWithProperties(0);
+        public EvaluateTestsClassWithProperties NewInstance => new EvaluateTestsClassWithProperties(3);
+
+        public EvaluateTestsClassWithProperties(int bias)
+        {
+            a = 4;
+            b = 0;
+            c = 0;
+            dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias);
+        }
+
+        public static async Task run()
+        {
+            var obj = new EvaluateTestsClassWithProperties(0);
+            var obj2 = new EvaluateTestsClassWithProperties(0);
+            obj.InstanceMethod(400, 123, "just a test", obj2);
+            new EvaluateTestsClassWithProperties(0).GenericInstanceMethod<int>(400, 123, "just a test", obj2);
+            new EvaluateTestsClassWithProperties(0).EvaluateShadow(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance);
+
+            await new EvaluateTestsClassWithProperties(0).InstanceMethodAsync(400, 123, "just a test", obj2);
+            await new EvaluateTestsClassWithProperties(0).GenericInstanceMethodAsync<int>(400, 123, "just a test", obj2);
+            await new EvaluateTestsClassWithProperties(0).EvaluateShadowAsync(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance);
+        }
+
+        public void EvaluateShadow(DateTime dateTime, EvaluateTestsClassWithProperties me)
+        {
+            string a = "hello";
+            Console.WriteLine($"Evaluate - break here");
+            SomeMethod(dateTime, me);
+        }
+
+        public async Task EvaluateShadowAsync(DateTime dateTime, EvaluateTestsClassWithProperties me)
+        {
+            string a = "hello";
+            Console.WriteLine($"EvaluateShadowAsync - break here");
+            await Task.CompletedTask;
+        }
+
+        public void SomeMethod(DateTime me, EvaluateTestsClassWithProperties dateTime)
+        {
+            Console.WriteLine($"break here");
+
+            var DTProp = "hello";
+            Console.WriteLine($"dtProp: {DTProp}");
+        }
+
+        public async Task InstanceMethodAsync(int g, int h, string valString, EvaluateTestsClassWithProperties me)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
             a = 1;
             b = 2;
             c = 3;
@@ -64,45 +137,176 @@ namespace DebuggerTests
             a = a + 1;
             b = b + 1;
             c = c + 1;
+            await Task.CompletedTask;
         }
 
-        public void GenericInstanceMethodOnStruct<T>(int g, int h, string valString)
+        public void InstanceMethod(int g, int h, string valString, EvaluateTestsClassWithProperties me)
         {
             int d = g + 1;
             int e = g + 2;
             int f = g + 3;
-            int i = d + e + f;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
             a = 1;
             b = 2;
             c = 3;
             dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            a = a + 1;
+            b = b + 1;
+            c = c + 1;
+        }
+
+        public void GenericInstanceMethod<T>(int g, int h, string valString, EvaluateTestsClassWithProperties me)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            a = a + 1;
+            b = b + 1;
+            c = c + 1;
             T t = default(T);
+        }
+
+        public async Task<T> GenericInstanceMethodAsync<T>(int g, int h, string valString, EvaluateTestsClassWithProperties me)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
             a = a + 1;
             b = b + 1;
             c = c + 1;
+            T t = default(T);
+            return await Task.FromResult(default(T));
         }
     }
 
-    public struct EvaluateTestsGenericStruct<T>
+    public struct EvaluateTestsStructWithProperties
     {
         public int a;
         public int b;
-        public int c;
-        DateTime dateTime;
-        public void EvaluateTestsGenericStructInstanceMethod(int g, int h, string valString)
+        public int c { get; set; }
+
+        public DateTime dateTime;
+        public DateTime DTProp => dateTime.AddMinutes(10);
+        public int IntProp => a + 5;
+        public string SetOnlyProp { set { a = value.Length; } }
+        public EvaluateTestsClassWithProperties NullIfAIsNotZero => a != 1908712 ? null : new EvaluateTestsClassWithProperties(0);
+        public EvaluateTestsStructWithProperties NewInstance => new EvaluateTestsStructWithProperties(3);
+
+        public EvaluateTestsStructWithProperties(int bias)
+        {
+            a = 4;
+            b = 0;
+            c = 0;
+            dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias);
+        }
+
+        public static async Task run()
+        {
+            var obj = new EvaluateTestsStructWithProperties(0);
+            var obj2 = new EvaluateTestsStructWithProperties(0);
+            obj.InstanceMethod(400, 123, "just a test", obj2);
+            new EvaluateTestsStructWithProperties(0).GenericInstanceMethod<int>(400, 123, "just a test", obj2);
+            new EvaluateTestsStructWithProperties(0).EvaluateShadow(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance);
+
+            await new EvaluateTestsStructWithProperties(0).InstanceMethodAsync(400, 123, "just a test", obj2);
+            await new EvaluateTestsStructWithProperties(0).GenericInstanceMethodAsync<int>(400, 123, "just a test", obj2);
+            await new EvaluateTestsStructWithProperties(0).EvaluateShadowAsync(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance);
+        }
+
+        public void EvaluateShadow(DateTime dateTime, EvaluateTestsStructWithProperties me)
+        {
+            string a = "hello";
+            Console.WriteLine($"Evaluate - break here");
+            SomeMethod(dateTime, me);
+        }
+
+        public async Task EvaluateShadowAsync(DateTime dateTime, EvaluateTestsStructWithProperties me)
+        {
+            string a = "hello";
+            Console.WriteLine($"EvaluateShadowAsync - break here");
+            await Task.CompletedTask;
+        }
+
+        public void SomeMethod(DateTime me, EvaluateTestsStructWithProperties dateTime)
+        {
+            Console.WriteLine($"break here");
+
+            var DTProp = "hello";
+            Console.WriteLine($"dtProp: {DTProp}");
+        }
+
+        public async Task InstanceMethodAsync(int g, int h, string valString, EvaluateTestsStructWithProperties me)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            a = a + 1;
+            b = b + 1;
+            c = c + 1;
+            await Task.CompletedTask;
+        }
+
+        public void InstanceMethod(int g, int h, string valString, EvaluateTestsStructWithProperties me)
         {
             int d = g + 1;
             int e = g + 2;
             int f = g + 3;
-            int i = d + e + f;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
             a = 1;
             b = 2;
             c = 3;
             dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            a = a + 1;
+            b = b + 1;
+            c = c + 1;
+        }
+
+        public void GenericInstanceMethod<T>(int g, int h, string valString, EvaluateTestsStructWithProperties me)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            a = a + 1;
+            b = b + 1;
+            c = c + 1;
             T t = default(T);
+        }
+
+        public async Task<T> GenericInstanceMethodAsync<T>(int g, int h, string valString, EvaluateTestsStructWithProperties me)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            var local_dt = new DateTime(2025, 3, 5, 7, 9, 11);
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
             a = a + 1;
-            b = b + 2;
-            c = c + 3;
+            b = b + 1;
+            c = c + 1;
+            T t = default(T);
+            return await Task.FromResult(default(T));
         }
     }
 }
index 0c5cc90..f557f4e 100644 (file)
@@ -11,7 +11,7 @@ namespace DebuggerTests
         public static void MethodWithLocalStructs()
         {
             var ss_local = new SimpleStruct("set in MethodWithLocalStructs", 1, DateTimeKind.Utc);
-            var gs_local = new GenericStruct<ValueTypesTest> { StringField = "gs_local#GenericStruct<ValueTypesTest>#StringField" };
+            var gs_local = new GenericStruct<ValueTypesTest> { StringField = $"gs_local#GenericStruct<ValueTypesTest>#StringField" };
 
             ValueTypesTest vt_local = new ValueTypesTest
             {
index 9353268..d3a6d39 100644 (file)
@@ -63,3 +63,16 @@ function negative_cfo_test (str_value = null) {
        console.log (`break here`);
        return ptd;
 }
+
+function eval_call_on_frame_test () {
+       let obj = {
+               a: 5,
+               b: "hello",
+               c: {
+                       c_x: 1
+               },
+       };
+
+       let obj_undefined = undefined;
+       console.log(`break here`);
+}
index d046a77..f5b506d 100644 (file)
@@ -361,6 +361,73 @@ var MonoSupportLib = {
                        return res;
                },
 
+        _resolve_member_by_name: function (base_object, base_name, expr_parts) {
+            if (base_object === undefined || base_object.value === undefined)
+                throw new Error(`Bug: base_object is undefined`);
+
+            if (base_object.value.type === 'object' && base_object.value.subtype === 'null')
+                throw new ReferenceError(`Null reference: ${base_name} is null`);
+
+            if (base_object.value.type !== 'object')
+                throw new ReferenceError(`'.' is only supported on non-primitive types. Failed on '${base_name}'`);
+
+            if (expr_parts.length == 0)
+                throw new Error(`Invalid member access expression`);//FIXME: need the full expression here
+
+            const root = expr_parts[0];
+            const props = this.mono_wasm_get_details(base_object.value.objectId, {});
+            let resObject = props.find(l => l.name == root);
+            if (resObject !== undefined) {
+                if (resObject.value === undefined && resObject.get !== undefined)
+                    resObject = this._invoke_getter(base_object.value.objectId, root);
+            }
+
+            if (resObject === undefined || expr_parts.length == 1)
+                return resObject;
+            else {
+                expr_parts.shift();
+                return this._resolve_member_by_name(resObject, root, expr_parts);
+            }
+        },
+
+        mono_wasm_eval_member_access: function (scope, var_list, rootObjectId, expr) {
+            if (expr === undefined || expr.length == 0)
+                throw new Error(`expression argument required`);
+
+            let parts = expr.split('.');
+            if (parts.length == 0)
+                throw new Error(`Invalid member access expression: ${expr}`);
+
+            const root = parts[0];
+
+            const locals = this.mono_wasm_get_variables(scope, var_list);
+            let rootObject = locals.find(l => l.name === root);
+            if (rootObject === undefined) {
+                // check `this`
+                const thisObject = locals.find(l => l.name == "this");
+                if (thisObject === undefined)
+                    throw new ReferenceError(`Could not find ${root} in locals, and no 'this' found.`);
+
+                const thisProps = this.mono_wasm_get_details(thisObject.value.objectId, {});
+                rootObject = thisProps.find(tp => tp.name == root);
+                if (rootObject === undefined)
+                    throw new ReferenceError(`Could not find ${root} in locals, or in 'this'`);
+
+                if (rootObject.value === undefined && rootObject.get !== undefined)
+                    rootObject = this._invoke_getter(thisObject.value.objectId, root);
+            }
+
+            parts.shift();
+
+            if (parts.length == 0)
+                return rootObject;
+
+            if (rootObject === undefined || rootObject.value === undefined)
+                throw new Error(`Could not get a value for ${root}`);
+
+            return this._resolve_member_by_name(rootObject, root, parts);
+        },
+
                /**
                 * @param  {WasmId} id
                 * @returns {object[]}