From 3822598be11fd3dbabc97455691bd002e2e4d559 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Thu, 28 Sep 2023 08:08:01 +0000 Subject: [PATCH] Changes from https://github.com/dotnet/runtime/pull/92630 --- .../BrowserDebugProxy/EvaluateExpression.cs | 4 +- .../MemberReferenceResolver.cs | 120 ++++++++++++------ .../EvaluateOnCallFrame2Tests.cs | 18 +++ .../EvaluateOnCallFrameTests.cs | 4 +- .../debugger-test/debugger-evaluate-test.cs | 6 +- 5 files changed, 106 insertions(+), 46 deletions(-) diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index c384f74e9c722..45d700e122ea7 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -387,12 +387,14 @@ private static async Task> ResolveElementAccess(ExpressionSyntaxR { var values = new List(); JObject index = null; + List nestedIndexers = new(); IEnumerable elementAccesses = replacer.elementAccess; foreach (ElementAccessExpressionSyntax elementAccess in elementAccesses.Reverse()) { - index = await resolver.Resolve(elementAccess, replacer.memberAccessValues, index, replacer.variableDefinitions, token); + index = await resolver.Resolve(elementAccess, replacer.memberAccessValues, nestedIndexers, replacer.variableDefinitions, token); if (index == null) throw new ReturnAsErrorException($"Failed to resolve element access for {elementAccess}", "ReferenceError"); + nestedIndexers.Add(index); } values.Add(index); return values; diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs index 650583a9dc7bf..e1b9583ddbe3e 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -366,7 +366,12 @@ async Task ResolveAsInstanceMember(ArraySegment parts, JObject } } - public async Task Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary memberAccessValues, JObject indexObject, List variableDefinitions, CancellationToken token) + public async Task Resolve( + ElementAccessExpressionSyntax elementAccess, + Dictionary memberAccessValues, + List nestedIndexObject, + List variableDefinitions, + CancellationToken token) { try { @@ -376,12 +381,13 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, if (rootObject == null) { - // it might be a jagged array where indexObject should be treated as a new rootObject - rootObject = indexObject; - indexObject = null; + // it might be a jagged array where the previously added nestedIndexObject should be treated as a new rootObject + rootObject = nestedIndexObject.LastOrDefault(); + if (rootObject != null) + nestedIndexObject.RemoveAt(nestedIndexObject.Count - 1); } - ElementIndexInfo elementIdxInfo = await GetElementIndexInfo(); + ElementIndexInfo elementIdxInfo = await GetElementIndexInfo(nestedIndexObject); if (elementIdxInfo is null) return null; @@ -394,6 +400,7 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, if (!DotnetObjectId.TryParse(rootObject?["objectId"]?.Value(), out DotnetObjectId objectId)) throw new InvalidOperationException($"Cannot apply indexing with [] to a primitive object of type '{type}'"); + bool isMultidimensional = elementIdxInfo.DimensionsCount != 1; switch (objectId.Scheme) { case "valuetype": //can be an inlined array @@ -407,7 +414,7 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, } case "array": rootObject["value"] = await context.SdbAgent.GetArrayValues(objectId.Value, token); - if (!elementIdxInfo.IsMultidimensional) + if (!isMultidimensional) { int.TryParse(elementIdxInfo.ElementIdxStr, out elementIdx); return (JObject)rootObject["value"][elementIdx]["value"]; @@ -417,10 +424,8 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, return (JObject)(((JArray)rootObject["value"]).FirstOrDefault(x => x["name"].Value() == elementIdxInfo.ElementIdxStr)["value"]); } case "object": - if (elementIdxInfo.IsMultidimensional) - throw new InvalidOperationException($"Cannot apply indexing with [,] to an object of type '{type}'"); // ToDo: try to use the get_Item for string as well - if (type == "string") + if (!isMultidimensional && type == "string") { var eaExpressionFormatted = elementAccessStrExpression.Replace('.', '_'); // instance_str variableDefinitions.Add(new (eaExpressionFormatted, rootObject, ExpressionEvaluator.ConvertJSToCSharpLocalVariableAssignment(eaExpressionFormatted, rootObject))); @@ -428,7 +433,7 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, var variableDef = await ExpressionEvaluator.GetVariableDefinitions(this, variableDefinitions, invokeToStringInObject: false, token); return await ExpressionEvaluator.EvaluateSimpleExpression(this, eaFormatted, elementAccessStr, variableDef, logger, token); } - if (indexObject is null && elementIdxInfo.IndexingExpression is null) + if (elementIdxInfo.Indexers is null || elementIdxInfo.Indexers.Count == 0) throw new InternalErrorException($"Unable to write index parameter to invoke the method in the runtime."); var typeIds = await context.SdbAgent.GetTypeIdsForObject(objectId.Value, true, token); @@ -441,15 +446,13 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, { MethodInfoWithDebugInformation methodInfo = await context.SdbAgent.GetMethodInfo(methodIds[i], token); ParameterInfo[] paramInfo = methodInfo.GetParametersInfo(); - if (paramInfo.Length == 1) + if (paramInfo.Length == elementIdxInfo.DimensionsCount) { try { - if (indexObject != null && !CheckParametersCompatibility(paramInfo[0].TypeCode, indexObject)) + if (!CheckParametersCompatibility(paramInfo, elementIdxInfo.Indexers)) continue; - ArraySegment buffer = indexObject is null ? - await WriteLiteralExpressionAsIndex(objectId, elementIdxInfo.IndexingExpression, elementIdxInfo.ElementIdxStr) : - await WriteJObjectAsIndex(objectId, indexObject, elementIdxInfo.ElementIdxStr, paramInfo[0].TypeCode); + ArraySegment buffer = await WriteIndexObjectAsIndices(objectId, elementIdxInfo.Indexers, paramInfo); JObject getItemRetObj = await context.SdbAgent.InvokeMethod(buffer, methodIds[i], token); return (JObject)getItemRetObj["value"]; } @@ -470,31 +473,32 @@ public async Task Resolve(ElementAccessExpressionSyntax elementAccess, throw new ReturnAsErrorException($"Unable to evaluate element access '{elementAccess}': {ex.Message}", ex.GetType().Name); } - async Task GetElementIndexInfo() + async Task GetElementIndexInfo(List nestedIndexers) { - // e.g. x[a[0]], x[a[b[1]]] etc. - if (indexObject is not null) - return new ElementIndexInfo(ElementIdxStr: indexObject["value"].ToString() ); - if (elementAccess.ArgumentList is null) return null; - StringBuilder elementIdxStr = new StringBuilder(); - var multiDimensionalArray = false; + int dimCnt = elementAccess.ArgumentList.Arguments.Count; LiteralExpressionSyntax indexingExpression = null; - for (int i = 0; i < elementAccess.ArgumentList.Arguments.Count; i++) + StringBuilder elementIdxStr = new StringBuilder(); + List indexers = new(); + // nesting should be resolved in reverse order + int nestedIndexersCnt = nestedIndexers.Count - 1; + for (int i = 0; i < dimCnt; i++) { + JObject indexObject; var arg = elementAccess.ArgumentList.Arguments[i]; if (i != 0) { elementIdxStr.Append(", "); - multiDimensionalArray = true; } // e.g. x[1] if (arg.Expression is LiteralExpressionSyntax) { indexingExpression = arg.Expression as LiteralExpressionSyntax; - elementIdxStr.Append(indexingExpression.ToString()); + string expression = indexingExpression.ToString(); + elementIdxStr.Append(expression); + indexers.Add(indexingExpression); } // e.g. x[a] or x[a.b] @@ -508,6 +512,18 @@ async Task GetElementIndexInfo() // x[a] indexObject ??= await Resolve(argParm.Identifier.Text, token); elementIdxStr.Append(indexObject["value"].ToString()); + indexers.Add(indexObject); + } + // nested indexing, e.g. x[a[0]], x[a[b[1]]], x[a[0], b[1]] + else if (arg.Expression is ElementAccessExpressionSyntax) + { + if (nestedIndexers == null || nestedIndexersCnt < 0) + throw new InvalidOperationException($"Cannot resolve nested indexing"); + JObject nestedIndexObject = nestedIndexers[nestedIndexersCnt]; + nestedIndexers.RemoveAt(nestedIndexersCnt); + elementIdxStr.Append(nestedIndexObject["value"].ToString()); + indexers.Add(nestedIndexObject); + nestedIndexersCnt--; } // indexing with expressions, e.g. x[a + 1] else @@ -519,36 +535,57 @@ async Task GetElementIndexInfo() if (idxType != "number") throw new InvalidOperationException($"Cannot index with an object of type '{idxType}'"); elementIdxStr.Append(indexObject["value"].ToString()); + indexers.Add(indexObject); } } return new ElementIndexInfo( + DimensionsCount: dimCnt, ElementIdxStr: elementIdxStr.ToString(), - IsMultidimensional: multiDimensionalArray, - IndexingExpression: indexingExpression); + Indexers: indexers); } - async Task> WriteJObjectAsIndex(DotnetObjectId rootObjId, JObject indexObject, string elementIdxStr, ElementType? expectedType) + async Task> WriteIndexObjectAsIndices(DotnetObjectId rootObjId, List indexObjects, ParameterInfo[] paramInfo) { using var writer = new MonoBinaryWriter(); writer.WriteObj(rootObjId, context.SdbAgent); - writer.Write(1); // number of method args - if (!await writer.WriteJsonValue(indexObject, context.SdbAgent, expectedType, token)) - throw new InternalErrorException($"Parsing index of type {indexObject["type"].Value()} to write it into the buffer failed."); + writer.Write(indexObjects.Count); // number of method args + foreach ((ParameterInfo pi, object indexObject) in paramInfo.Zip(indexObjects)) + { + if (indexObject is JObject indexJObject) + { + // indexed by an identifier name syntax + if (!await writer.WriteJsonValue(indexJObject, context.SdbAgent, pi.TypeCode, token)) + throw new InternalErrorException($"Parsing index of type {indexJObject["type"].Value()} to write it into the buffer failed."); + } + else if (indexObject is LiteralExpressionSyntax expression) + { + // indexed by a literal expression syntax + if (!await writer.WriteConst(expression, context.SdbAgent, token)) + throw new InternalErrorException($"Parsing literal expression index = {expression} to write it into the buffer failed."); + } + else + { + throw new InternalErrorException($"Unexpected index type."); + } + } return writer.GetParameterBuffer(); } + } - async Task> WriteLiteralExpressionAsIndex(DotnetObjectId rootObjId, LiteralExpressionSyntax indexingExpression, string elementIdxStr) + private static bool CheckParametersCompatibility(ParameterInfo[] paramInfos, List indexObjects) + { + if (paramInfos.Length != indexObjects.Count) + return false; + foreach ((ParameterInfo paramInfo, object indexObj) in paramInfos.Zip(indexObjects)) { - using var writer = new MonoBinaryWriter(); - writer.WriteObj(rootObjId, context.SdbAgent); - writer.Write(1); // number of method args - if (!await writer.WriteConst(indexingExpression, context.SdbAgent, token)) - throw new InternalErrorException($"Parsing index of type {indexObject["type"].Value()} to write it into the buffer failed."); - return writer.GetParameterBuffer(); + // shouldn't we check LiteralExpressionSyntax for compatibility as well? + if (indexObj is JObject indexJObj && !CheckParameterCompatibility(paramInfo.TypeCode, indexJObj)) + return false; } + return true; } - private static bool CheckParametersCompatibility(ElementType? paramTypeCode, JObject value) + private static bool CheckParameterCompatibility(ElementType? paramTypeCode, JObject value) { if (!paramTypeCode.HasValue) return true; @@ -871,7 +908,8 @@ public JObject TryGetEvaluationResult(string id) private sealed record ElementIndexInfo( string ElementIdxStr, - bool IsMultidimensional = false, - LiteralExpressionSyntax IndexingExpression = null); + // keeps JObjects and LiteralExpressionSyntaxes: + List Indexers, + int DimensionsCount = 1); } } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrame2Tests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrame2Tests.cs index 051da33469ce2..b1a79b28ceeef 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrame2Tests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrame2Tests.cs @@ -731,5 +731,23 @@ public async Task EvaluateLocalObjectFromAssemblyNotRelatedButLoaded() ("dt+1", "Cannot evaluate '(dt+1\n)': (2,9): error CS0019: Operator '+' cannot be applied to operands of type 'object' and 'int'") ); }); + + [Fact] + public async Task EvaluateObjectIndexingMultidimensional() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateLocalsWithIndexingTests", "EvaluateLocals", 12, "DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateLocalsWithIndexingTests:EvaluateLocals'); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + await EvaluateOnCallFrameAndCheck(id, + ("f[j, aDouble]", TNumber("3.34")), //only IdentifierNameSyntaxes + ("f[1, aDouble]", TNumber("3.34")), //IdentifierNameSyntax with LiteralExpressionSyntax + ("f[aChar, \"&\", longString]", TString("9-&-longString")), + ("f[f.numArray[j], aDouble]", TNumber("4.34")), //ElementAccessExpressionSyntax + ("f[f.numArray[j], f.numArray[0]]", TNumber("3")), //multiple ElementAccessExpressionSyntaxes + ("f[f.numArray[f.numList[0]], f.numArray[i]]", TNumber("3")), + ("f[f.numArray[f.numList[0]], f.numArray[f.numArray[i]]]", TNumber("4")) + ); + }); } } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index 2ff9bd26a2827..2d0fb87822758 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -585,7 +585,7 @@ public async Task EvaluatePropertyThatThrows() Assert.Equal("Unable to evaluate element access 'f.idx0[2]': Cannot apply indexing with [] to a primitive object of type 'number'", res.Error["result"]?["description"]?.Value()); var exceptionDetailsStack = res.Error["exceptionDetails"]?["stackTrace"]?["callFrames"]?[0]; Assert.Equal("DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", exceptionDetailsStack?["functionName"]?.Value()); - Assert.Equal(556, exceptionDetailsStack?["lineNumber"]?.Value()); + Assert.Equal(558, exceptionDetailsStack?["lineNumber"]?.Value()); Assert.Equal(12, exceptionDetailsStack?["columnNumber"]?.Value()); (_, res) = await EvaluateOnCallFrame(id, "f[1]", expect_ok: false ); Assert.Equal( "Unable to evaluate element access 'f[1]': Cannot apply indexing with [] to an object of type 'DebuggerTests.EvaluateLocalsWithIndexingTests.TestEvaluate'", res.Error["result"]?["description"]?.Value()); @@ -722,7 +722,7 @@ public async Task EvaluatePropertyThatThrows() Assert.Equal("Unable to evaluate element access 'f.numList[\"a\" + 1]': Cannot index with an object of type 'string'", res.Error["result"]?["description"]?.Value()); var exceptionDetailsStack = res.Error["exceptionDetails"]?["stackTrace"]?["callFrames"]?[0]; Assert.Equal("DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", exceptionDetailsStack?["functionName"]?.Value()); - Assert.Equal(556, exceptionDetailsStack?["lineNumber"]?.Value()); + Assert.Equal(558, exceptionDetailsStack?["lineNumber"]?.Value()); Assert.Equal(12, exceptionDetailsStack?["columnNumber"]?.Value()); }); diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs b/src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs index 480dc30115c43..e46177ecc925b 100644 --- a/src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test/debugger-evaluate-test.cs @@ -522,7 +522,6 @@ public class TestEvaluate public int idx0; public int idx1; - // ToDo: add 2d indexing - https://github.com/dotnet/runtime/issues/76062 public string this[char key] => "res_" + key; public string this[bool key] => key.ToString(); public bool this[string key] => key.Length > 3; @@ -530,11 +529,14 @@ public class TestEvaluate public int this[float key] => (int)key; public int this[decimal key] => (int)key; + public double this[int key1, double key2] => key1 + key2; + public string this[char key1, string key2, string key3] => $"{key1}-{key2}-{key3}"; + public void run() { numList = new List { 1, 2 }; textList = new List { "1", "2" }; - numArray = new int[] { 1, 2 }; + numArray = new int[] { 1, 2, 0 }; textArray = new string[] { "1", "2" }; numArrayOfArrays = new int[][] { numArray, numArray }; numListOfLists = new List> { numList, numList };