Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/8.0][wasm][debugger] Support multidimensional indexing of object scheme #92753

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -387,12 +387,14 @@ private static async Task<IList<JObject>> ResolveElementAccess(ExpressionSyntaxR
{
var values = new List<JObject>();
JObject index = null;
List<JObject> nestedIndexers = new();
IEnumerable<ElementAccessExpressionSyntax> 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;
Expand Down
120 changes: 79 additions & 41 deletions src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,12 @@ async Task<JObject> ResolveAsInstanceMember(ArraySegment<string> parts, JObject
}
}

public async Task<JObject> Resolve(ElementAccessExpressionSyntax elementAccess, Dictionary<string, JObject> memberAccessValues, JObject indexObject, List<VariableDefinition> variableDefinitions, CancellationToken token)
public async Task<JObject> Resolve(
ElementAccessExpressionSyntax elementAccess,
Dictionary<string, JObject> memberAccessValues,
List<JObject> nestedIndexObject,
List<VariableDefinition> variableDefinitions,
CancellationToken token)
{
try
{
Expand All @@ -376,12 +381,13 @@ public async Task<JObject> 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;

Expand All @@ -394,6 +400,7 @@ public async Task<JObject> Resolve(ElementAccessExpressionSyntax elementAccess,
if (!DotnetObjectId.TryParse(rootObject?["objectId"]?.Value<string>(), 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
Expand All @@ -407,7 +414,7 @@ public async Task<JObject> 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"];
Expand All @@ -417,18 +424,16 @@ public async Task<JObject> Resolve(ElementAccessExpressionSyntax elementAccess,
return (JObject)(((JArray)rootObject["value"]).FirstOrDefault(x => x["name"].Value<string>() == 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)));
var eaFormatted = elementAccessStr.Replace('.', '_'); // instance_str[1]
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);
Expand All @@ -441,15 +446,13 @@ public async Task<JObject> 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<byte> buffer = indexObject is null ?
await WriteLiteralExpressionAsIndex(objectId, elementIdxInfo.IndexingExpression, elementIdxInfo.ElementIdxStr) :
await WriteJObjectAsIndex(objectId, indexObject, elementIdxInfo.ElementIdxStr, paramInfo[0].TypeCode);
ArraySegment<byte> buffer = await WriteIndexObjectAsIndices(objectId, elementIdxInfo.Indexers, paramInfo);
JObject getItemRetObj = await context.SdbAgent.InvokeMethod(buffer, methodIds[i], token);
return (JObject)getItemRetObj["value"];
}
Expand All @@ -470,31 +473,32 @@ await WriteLiteralExpressionAsIndex(objectId, elementIdxInfo.IndexingExpression,
throw new ReturnAsErrorException($"Unable to evaluate element access '{elementAccess}': {ex.Message}", ex.GetType().Name);
}

async Task<ElementIndexInfo> GetElementIndexInfo()
async Task<ElementIndexInfo> GetElementIndexInfo(List<JObject> 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<object> 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]
Expand All @@ -508,6 +512,18 @@ async Task<ElementIndexInfo> 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
Expand All @@ -519,36 +535,57 @@ async Task<ElementIndexInfo> 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<ArraySegment<byte>> WriteJObjectAsIndex(DotnetObjectId rootObjId, JObject indexObject, string elementIdxStr, ElementType? expectedType)
async Task<ArraySegment<byte>> WriteIndexObjectAsIndices(DotnetObjectId rootObjId, List<object> 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<string>()} 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<string>()} 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<ArraySegment<byte>> WriteLiteralExpressionAsIndex(DotnetObjectId rootObjId, LiteralExpressionSyntax indexingExpression, string elementIdxStr)
private static bool CheckParametersCompatibility(ParameterInfo[] paramInfos, List<object> 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<string>()} 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;
Expand Down Expand Up @@ -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<object> Indexers,
int DimensionsCount = 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -731,5 +731,23 @@ await CheckEvaluateFail(id,
("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<string>();
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"))
);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ public async Task EvaluateIndexingNegative() => await CheckInspectLocalsAtBreakp
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<string>());
var exceptionDetailsStack = res.Error["exceptionDetails"]?["stackTrace"]?["callFrames"]?[0];
Assert.Equal("DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", exceptionDetailsStack?["functionName"]?.Value<string>());
Assert.Equal(556, exceptionDetailsStack?["lineNumber"]?.Value<int>());
Assert.Equal(558, exceptionDetailsStack?["lineNumber"]?.Value<int>());
Assert.Equal(12, exceptionDetailsStack?["columnNumber"]?.Value<int>());
(_, 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<string>());
Expand Down Expand Up @@ -722,7 +722,7 @@ public async Task EvaluateIndexingByExpressionNegative() => await CheckInspectLo
Assert.Equal("Unable to evaluate element access 'f.numList[\"a\" + 1]': Cannot index with an object of type 'string'", res.Error["result"]?["description"]?.Value<string>());
var exceptionDetailsStack = res.Error["exceptionDetails"]?["stackTrace"]?["callFrames"]?[0];
Assert.Equal("DebuggerTests.EvaluateLocalsWithIndexingTests.EvaluateLocals", exceptionDetailsStack?["functionName"]?.Value<string>());
Assert.Equal(556, exceptionDetailsStack?["lineNumber"]?.Value<int>());
Assert.Equal(558, exceptionDetailsStack?["lineNumber"]?.Value<int>());
Assert.Equal(12, exceptionDetailsStack?["columnNumber"]?.Value<int>());
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,19 +522,21 @@ 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;
public int this[double key] => (int)key;
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<int> { 1, 2 };
textList = new List<string> { "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<List<int>> { numList, numList };
Expand Down
Loading