Skip to content

Commit

Permalink
Handle evaluate request for complex variables (#304)
Browse files Browse the repository at this point in the history
For simple variables, evaluateRequest() returns the value.

For complex variables such as arrays and structures, a
different representation is returned 
(arrays = [sizeof(array)]; structs = {...}). This PR adds the 
ability to evaluate a complex variable (at least 1 level 
deep) which is helpful for the variables context menu 
-> Copy Value command.

Adds some tests as well.

Signed-off-by: Thor Thayer <thor.thayer@ericsson.com>
Co-authored-by: Thor Thayer <thor.thayer@ericsson.com>
  • Loading branch information
WyoTwT and WyoTwT authored Oct 10, 2023
1 parent 86c1e77 commit 16134b0
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 13 deletions.
85 changes: 79 additions & 6 deletions src/GDBDebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ class ThreadWithStatus implements DebugProtocol.Thread {
const ignoreCountRegex = /\s|>/g;
const arrayRegex = /.*\[[\d]+\].*/;
const arrayChildRegex = /[\d]+/;
const numberRegex = /^-?\d+(?:\.\d*)?$/; // match only numbers (integers and floats)
const cNumberTypeRegex = /\b(?:char|short|int|long|float|double)$/; // match C number types
const cBoolRegex = /\bbool$/; // match boolean

export function hexToBase64(hex: string): string {
// The buffer will ignore incomplete bytes (unpaired digits), so we need to catch that early
Expand Down Expand Up @@ -1347,8 +1350,12 @@ export class GDBDebugSession extends LoggingDebugSession {
}
}
if (varobj) {
const result =
args.context === 'variables' && Number(varobj.numchild)
? await this.getChildElements(varobj, args.frameId)
: varobj.value;
response.body = {
result: varobj.value,
result,
type: varobj.type,
variablesReference:
parseInt(varobj.numchild, 10) > 0
Expand All @@ -1371,6 +1378,49 @@ export class GDBDebugSession extends LoggingDebugSession {
}
}

protected async getChildElements(varobj: VarObjType, frameHandle: number) {
if (Number(varobj.numchild) > 0) {
const objRef: ObjectVariableReference = {
type: 'object',
frameHandle: frameHandle,
varobjName: varobj.varname,
};
const childVariables: DebugProtocol.Variable[] =
await this.handleVariableRequestObject(objRef);
const value = arrayChildRegex.test(varobj.type)
? childVariables.map<string | number | boolean>((child) =>
this.convertValue(child)
)
: childVariables.reduce<
Record<string, string | number | boolean>
>(
(accum, child) => (
(accum[child.name] = this.convertValue(child)), accum
),
{}
);
return JSON.stringify(value, null, 2);
}
return varobj.value;
}

protected convertValue(variable: DebugProtocol.Variable) {
const varValue = variable.value;
const varType = String(variable.type);
if (cNumberTypeRegex.test(varType)) {
if (numberRegex.test(varValue)) {
return Number(varValue);
} else {
// probably a string/other representation
return String(varValue);
}
} else if (cBoolRegex.test(varType)) {
return Boolean(varValue);
} else {
return varValue;
}
}

/**
* Implement the cdt-gdb-adapter/Memory request.
*/
Expand Down Expand Up @@ -1872,6 +1922,7 @@ export class GDBDebugSession extends LoggingDebugSession {
}
variables.push({
name: varobj.expression,
evaluateName: varobj.expression,
value,
type: varobj.type,
memoryReference: `&(${varobj.expression})`,
Expand Down Expand Up @@ -1944,6 +1995,7 @@ export class GDBDebugSession extends LoggingDebugSession {
}
variables.push({
name: varobj.expression,
evaluateName: varobj.expression,
value,
type: varobj.type,
memoryReference: `&(${varobj.expression})`,
Expand Down Expand Up @@ -2000,6 +2052,10 @@ export class GDBDebugSession extends LoggingDebugSession {
printValues: mi.MIVarPrintValues.all,
});
}
// Grab the full path of parent.
const topLevelPathExpression =
varobj?.expression ??
(await this.getFullPathExpression(parentVarname));

// iterate through the children
for (const child of children.children) {
Expand All @@ -2011,10 +2067,13 @@ export class GDBDebugSession extends LoggingDebugSession {
name,
printValues: mi.MIVarPrintValues.all,
});
// Append the child path to the top level full path.
const parentClassName = `${topLevelPathExpression}.${child.exp}`;
for (const objChild of objChildren.children) {
const childName = `${name}.${objChild.exp}`;
variables.push({
name: objChild.exp,
evaluateName: `${parentClassName}.${objChild.exp}`,
value: objChild.value ? objChild.value : objChild.type,
type: objChild.type,
variablesReference:
Expand Down Expand Up @@ -2045,22 +2104,21 @@ export class GDBDebugSession extends LoggingDebugSession {
if (isArrayParent || isArrayChild) {
// can't use a relative varname (eg. var1.a.b.c) to create/update a new var so fetch and track these
// vars by evaluating their path expression from GDB
const exprResponse = await mi.sendVarInfoPathExpression(
this.gdb,
const fullPath = await this.getFullPathExpression(
child.name
);
// create or update the var in GDB
let arrobj = this.gdb.varManager.getVar(
frame.frameId,
frame.threadId,
depth,
exprResponse.path_expr
fullPath
);
if (!arrobj) {
const varCreateResponse = await mi.sendVarCreate(
this.gdb,
{
expression: exprResponse.path_expr,
expression: fullPath,
frameId: frame.frameId,
threadId: frame.threadId,
}
Expand All @@ -2069,7 +2127,7 @@ export class GDBDebugSession extends LoggingDebugSession {
frame.frameId,
frame.threadId,
depth,
exprResponse.path_expr,
fullPath,
true,
false,
varCreateResponse
Expand All @@ -2090,8 +2148,13 @@ export class GDBDebugSession extends LoggingDebugSession {
varobjName = arrobj.varname;
}
const variableName = isArrayChild ? name : child.exp;
const evaluateName =
isArrayParent || isArrayChild
? await this.getFullPathExpression(child.name)
: `${topLevelPathExpression}.${child.exp}`;
variables.push({
name: variableName,
evaluateName,
value,
type: child.type,
variablesReference:
Expand All @@ -2108,6 +2171,16 @@ export class GDBDebugSession extends LoggingDebugSession {
return Promise.resolve(variables);
}

/** Query GDB using varXX name to get complete variable name */
protected async getFullPathExpression(inputVarName: string) {
const exprResponse = await mi.sendVarInfoPathExpression(
this.gdb,
inputVarName
);
// result from GDB looks like (parentName).field so remove ().
return exprResponse.path_expr.replace(/[()]/g, '');
}

// Register view
// Assume that the register name are unchanging over time, and the same across all threadsf
private registerMap = new Map<string, number>();
Expand Down
11 changes: 10 additions & 1 deletion src/integration-tests/test-programs/vars.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,32 @@ struct bar
int b;
};

struct baz
{
float w;
double v;
};

struct foo
{
int x;
int y;
struct bar z;
struct baz aa;
};

int main()
{
int a = 1;
int b = 2;
int c = a + b; // STOP HERE
struct foo r = {1, 2, {3, 4}};
struct foo r = {1, 2, {3, 4}, {3.1415, 1234.5678}};
int d = r.x + r.y;
int e = r.z.a + r.z.b;
int f[] = {1, 2, 3};
int g = f[0] + f[1] + f[2]; // After array init
int rax = 1;
const unsigned char h[] = {0x01, 0x10, 0x20};
const unsigned char k[] = "hello"; // char string setup
return 0;
}
78 changes: 72 additions & 6 deletions src/integration-tests/var.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ describe('Variables Test Suite', function () {
let scope: Scope;
const varsProgram = path.join(testProgramsDir, 'vars');
const varsSrc = path.join(testProgramsDir, 'vars.c');
const numVars = 9; // number of variables in the main() scope of vars.c
const numVars = 11; // number of variables in the main() scope of vars.c

const lineTags = {
'STOP HERE': 0,
'After array init': 0,
'char string setup': 0,
};

const hexValueRegex = /^0x[\da-fA-F]+$/;
Expand Down Expand Up @@ -215,7 +216,7 @@ describe('Variables Test Suite', function () {
expect(
children.body.variables.length,
'There is a different number of child variables than expected'
).to.equal(3);
).to.equal(4);
verifyVariable(children.body.variables[0], 'x', 'int', '1', {
hasMemoryReference: false,
});
Expand Down Expand Up @@ -250,7 +251,7 @@ describe('Variables Test Suite', function () {
expect(
children.body.variables.length,
'There is a different number of child variables than expected'
).to.equal(3);
).to.equal(4);
verifyVariable(children.body.variables[0], 'x', 'int', '25', {
hasMemoryReference: false,
});
Expand Down Expand Up @@ -308,7 +309,7 @@ describe('Variables Test Suite', function () {
expect(
children.body.variables.length,
'There is a different number of child variables than expected'
).to.equal(3);
).to.equal(4);
verifyVariable(children.body.variables[2], 'z', 'struct bar', '{...}', {
hasChildren: true,
hasMemoryReference: false,
Expand All @@ -328,6 +329,23 @@ describe('Variables Test Suite', function () {
verifyVariable(subChildren.body.variables[1], 'b', 'int', '4', {
hasMemoryReference: false,
});

// Evaluate the child structure foo.bar of r
let res = await dc.evaluateRequest({
context: 'variables',
expression: 'r.z',
frameId: scope.frame.id,
});
expect(res.body.result).eq('{\n "a": 3,\n "b": 4\n}');

// Evaluate the child structure foo.baz of r
res = await dc.evaluateRequest({
context: 'variables',
expression: 'r.aa',
frameId: scope.frame.id,
});
expect(res.body.result).eq('{\n "w": 3.1415,\n "v": 1234.5678\n}');

// set the variables to something different
const setAinHex = await dc.setVariableRequest({
name: 'a',
Expand Down Expand Up @@ -392,7 +410,7 @@ describe('Variables Test Suite', function () {
});
expect(br.success).to.equal(true);
await dc.continue({ threadId: scope.thread.id }, 'breakpoint', {
line: 24,
line: lineTags['After array init'],
path: varsSrc,
});
scope = await getScopes(dc);
Expand Down Expand Up @@ -474,7 +492,7 @@ describe('Variables Test Suite', function () {
// step the program and see that the values were passed to the program and evaluated.
await dc.next(
{ threadId: scope.thread.id },
{ path: varsSrc, line: 25 }
{ path: varsSrc, line: lineTags['After array init'] + 1 }
);
scope = await getScopes(dc);
expect(
Expand All @@ -489,4 +507,52 @@ describe('Variables Test Suite', function () {
).to.equal(numVars);
verifyVariable(vars.body.variables[7], 'g', 'int', '66');
});

it('can evaluate char array elements (string)', async function () {
// skip ahead to array initialization
const br = await dc.setBreakpointsRequest({
source: { path: varsSrc },
breakpoints: [{ line: lineTags['char string setup'] }],
});
expect(br.success).to.equal(true);
await dc.continue({ threadId: scope.thread.id }, 'breakpoint', {
line: lineTags['char string setup'],
path: varsSrc,
});
// step the program and see that the values were passed to the program and evaluated.
await dc.next(
{ threadId: scope.thread.id },
{ path: varsSrc, line: lineTags['char string setup'] + 1 }
);
scope = await getScopes(dc);
expect(
scope.scopes.body.scopes.length,
'Unexpected number of scopes returned'
).to.equal(2);
// assert we can see the array and its elements
const vr = scope.scopes.body.scopes[0].variablesReference;
const vars = await dc.variablesRequest({ variablesReference: vr });
expect(
vars.body.variables.length,
'There is a different number of variables than expected'
).to.equal(numVars);
// Evaluate the non-string char array
let res = await dc.evaluateRequest({
context: 'variables',
expression: 'h',
frameId: scope.frame.id,
});
expect(res.body.result).eq(
'[\n "1 \'\\\\001\'",\n "16 \'\\\\020\'",\n "32 \' \'"\n]'
);
// Evaluate the string char array
res = await dc.evaluateRequest({
context: 'variables',
expression: 'k',
frameId: scope.frame.id,
});
expect(res.body.result).eq(
'[\n "104 \'h\'",\n "101 \'e\'",\n "108 \'l\'",\n "108 \'l\'",\n "111 \'o\'",\n "0 \'\\\\000\'"\n]'
);
});
});

0 comments on commit 16134b0

Please sign in to comment.