Expression format specifier support (#87)

* initial support for format specifier

* add readme notes for format specifiers

* add support for showing arrays and objects  with format specifiers

* create unique object variable references for different display formats

* add notes on applying formatting to objects and arrays
This commit is contained in:
Dave Holoway
2020-04-24 14:37:51 +01:00
committed by GitHub
parent 1535f133d9
commit a4ce09d309
5 changed files with 219 additions and 86 deletions

View File

@@ -129,6 +129,39 @@ Add a new task to run the build command:
}
```
## Expression evaluation
Format specifiers can be appended to watch and repl expressions to change how the evaluated result is displayed.
The specifiers work with the same syntax used in Visual Studio.
See https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp for examples.
```
123 123
123,x 0x0000007b
123,xb 0000007b
123,X 0x0000007B
123,o 000000000173
123,b 0b00000000000000000000000001111011
123,bb 00000000000000000000000001111011
123,c '{'
"one\ntwo" "one\ntwo"
"one\ntwo",sb one\ntwo
"one\ntwo",! one
two
```
You can also apply the specifiers to object and array instances to format fields and elements:
```
arr,x int[3]
[0] 0x00000001
[1] 0x00000002
[1] 0x00000003
```
Note: Format specifiers for floating point values (`e`/`g`) and string encoding conversions (`s8`/`su`/`s32`) are not supported.
## Powered by coffee
The Android Developer Extension is a completely free, fully open-source project. If you've found the extension useful, you

View File

@@ -79,7 +79,10 @@ class AndroidDebugSession extends DebugSession {
// number of call stack entries to display above the project source
this.callStackDisplaySize = 1;
// the fifo queue of evaluations (watches, hover, etc)
/**
* the fifo queue of evaluations (watches, hover, etc)
* @type {EvalQueueEntry[]}
*/
this._evals_queue = [];
// since we want to send breakpoint events, we will assign an id to every event
@@ -1269,7 +1272,7 @@ class AndroidDebugSession extends DebugSession {
const stack_frame = thread.findStackFrame(args.variablesReference);
// evaluate the expression
const locals = await stack_frame.getLocals();
const value = await evaluate(args.value, thread, locals, this.dbgr);
const { value } = await evaluate(args.value, thread, locals, this.dbgr);
// update the variable
const vsvar = await stack_frame.setVariableValue(args.variablesReference, args.name, value);
response.body = {
@@ -1307,25 +1310,15 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(prev.response);
}
const eval_info = {
expression: args.expression,
response,
/** @type {DebuggerValue[]} */
locals: null,
/** @type {VariableManager} */
var_manager: null,
/** @type {AndroidThread} */
thread: null,
}
let eval_info;
if (args.frameId) {
const threadId = AndroidThread.variableRefToThreadId(args.frameId);
const thread = this.getThread(threadId);
if (!thread) return this.failRequestNoThread('Evaluate',threadId, response);
if (!thread.paused) return this.failRequestThreadNotSuspended('Evaluate',threadId, response);
eval_info.thread = thread;
const stack_frame = thread.findStackFrame(args.frameId);
eval_info.var_manager = stack_frame;
eval_info.locals = await stack_frame.getLocals();
const locals = await stack_frame.getLocals();
eval_info = new EvalQueueEntry(args.expression, response, locals, stack_frame, thread);
} else {
// if there's no frameId, we are being asked to evaluate the value in the 'global' context.
// This is a problem because there's no associated stack frame, so we include any locals in the evaluation.
@@ -1334,9 +1327,7 @@ class AndroidDebugSession extends DebugSession {
// would require primitive literals)
const thread = this._threads.find(t => t && t.paused);
if (!thread) return this.failRequest(`No threads are paused`, response);
eval_info.thread = thread;
eval_info.var_manager = thread.getGlobalVariableManager();
eval_info.locals = [];
eval_info = new EvalQueueEntry(args.expression, response, [], thread.getGlobalVariableManager(), thread);
}
const queue_len = this._evals_queue.push(eval_info);
@@ -1347,8 +1338,8 @@ class AndroidDebugSession extends DebugSession {
while (this._evals_queue.length > 0) {
const { expression, response, locals, var_manager, thread } = this._evals_queue[0];
try {
const value = await evaluate(expression, thread, locals, this.dbgr);
const v = var_manager.makeVariableValue(value);
const { value, display_format } = await evaluate(expression, thread, locals, this.dbgr, { allowFormatSpecifier:true });
const v = var_manager.makeVariableValue(value, display_format);
response.body = {
result: v.value,
variablesReference: v.variablesReference|0
@@ -1363,6 +1354,23 @@ class AndroidDebugSession extends DebugSession {
}
}
class EvalQueueEntry {
/**
* @param {string} expression
* @param {import('vscode-debugprotocol').DebugProtocol.EvaluateResponse} response
* @param {DebuggerValue[]} locals
* @param {VariableManager} var_manager
* @param {AndroidThread} thread
*/
constructor(expression, response, locals, var_manager, thread) {
this.expression = expression;
this.response = response;
this.locals = locals;
this.var_manager = var_manager;
this.thread = thread;
}
}
/**
* @param {string} p
*/

View File

@@ -471,7 +471,7 @@ async function evaluate_binary_expression(dbgr, locals, thread, lhs, rhs, operat
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} operator
* @param {*} expr
* @param {ParsedExpression} expr
*/
async function evaluate_unary_expression(dbgr, locals, thread, operator, expr) {
/** @type {DebuggerValue} */
@@ -684,7 +684,7 @@ function evaluate_expression(dbgr, locals, thread, expr) {
* @param {Debugger} dbgr
* @param {DebuggerValue[]} locals
* @param {AndroidThread} thread
* @param {string} index_expr
* @param {ParsedExpression} index_expr
* @param {DebuggerValue} arr_local
*/
async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_local) {
@@ -712,36 +712,36 @@ async function evaluate_array_element(dbgr, locals, thread, index_expr, arr_loca
/**
* Build a regular expression which matches the possible parameter types for a value
* @param {Debugger} dbgr
* @param {DebuggerValue} v
* @param {DebuggerValue} argument
*/
async function getParameterSignatureRegex(dbgr, v) {
if (v.type.signature == 'Lnull;') {
async function getParameterSignatureRegex(dbgr, argument) {
if (argument.type.signature == 'Lnull;') {
return /^[LT[]/; // null matches any reference type
}
if (/^L/.test(v.type.signature)) {
if (/^L/.test(argument.type.signature)) {
// for class reference types, retrieve a list of inherited classes
// since subclass instances can be passed as arguments
const sigs = await dbgr.getClassInheritanceList(v.type.signature);
const sigs = await dbgr.getClassInheritanceList(argument.type.signature);
const re_sigs = sigs.map(signature => signature.replace(/[$]/g, '\\$'));
return new RegExp(`(^${re_sigs.join('$)|(^')}$)`);
}
if (/^\[/.test(v.type.signature)) {
if (/^\[/.test(argument.type.signature)) {
// for array types, only an exact array match or Object is allowed
return new RegExp(`^(${v.type.signature})|(${JavaType.Object.signature})$`);
return new RegExp(`^(${argument.type.signature})|(${JavaType.Object.signature})$`);
}
switch(v.type.signature) {
switch(argument.type.signature) {
case 'I':
// match bytes/shorts/ints/longs/floats/doubles literals within range
if (v.value >= -128 && v.value <= 127)
if (argument.value >= -128 && argument.value <= 127)
return /^[BSIJFD]$/
if (v.value >= -32768 && v.value <= 32767)
if (argument.value >= -32768 && argument.value <= 32767)
return /^[SIJFD]$/
return /^[IJFD]$/;
case 'F':
return /^[FD]$/; // floats can be assigned to floats or doubles
default:
// anything else must be an exact match (no implicit cast is valid)
return new RegExp(`^${v.type.signature}$`);
return new RegExp(`^${argument.type.signature}$`);
}
}
@@ -950,8 +950,9 @@ async function evaluate_cast(dbgr, locals, thread, cast_type, rhs) {
* @param {AndroidThread} thread
* @param {DebuggerValue[]} locals
* @param {Debugger} dbgr
* @param {{allowFormatSpecifier:boolean}} [options]
*/
async function evaluate(expression, thread, locals, dbgr) {
async function evaluate(expression, thread, locals, dbgr, options) {
D('evaluate: ' + expression);
await dbgr.ensureConnected();
@@ -967,6 +968,17 @@ async function evaluate(expression, thread, locals, dbgr) {
}
const parsed_expression = parse_expression(e);
let display_format = null;
if (options && options.allowFormatSpecifier) {
// look for formatting specifiers in the form of ',<x>'
// ref: https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp
const df_match = e.expr.match(/^,([doc!]|[xX]b?|bb?|sb?)/);
if (df_match) {
display_format = df_match[1];
e.expr = e.expr.slice(df_match[0].length)
}
}
// if there's anything left, it's an error
if (!parsed_expression || e.expr) {
// the expression is not well-formed
@@ -975,7 +987,11 @@ async function evaluate(expression, thread, locals, dbgr) {
// the expression is well-formed - start the (asynchronous) evaluation
const value = await evaluate_expression(dbgr, locals, thread, parsed_expression);
return value;
return {
value,
display_format,
}
}
module.exports = {

View File

@@ -53,7 +53,7 @@ class DebuggerStackFrame extends VariableManager {
}
const fetch_locals = async () => {
const values = await this.dbgr.getLocals(this.frame);
// display the variables in (case-insensitive) alphabetical order, with 'this' first and all-caps last
// display the variables in (case-insensitive) alphabetical order, with 'this' first
return this.locals = sortVariables(values, true, false);
}
// @ts-ignore
@@ -133,7 +133,7 @@ class DebuggerStackFrame extends VariableManager {
values = [await this.getBigString(varinfo)];
}
return (varinfo.cached = values).map(v => this.makeVariableValue(v));
return (varinfo.cached = values).map(v => this.makeVariableValue(v, varinfo.display_format));
}
async getObjectFields(varinfo) {

View File

@@ -12,8 +12,6 @@ class VariableManager {
* @param {VSCVariableReference} base_variable_reference The reference value for values stored by this manager
*/
constructor(base_variable_reference) {
// expandable variables get allocated new variable references.
this._expandable_prims = false;
/** @type {VSCVariableReference} */
this.nextVariableRef = base_variable_reference + 10;
@@ -40,10 +38,20 @@ class VariableManager {
this.variableValues.set(variablesReference, value);
}
_getObjectIdReference(type, objvalue) {
/**
* Retrieve or create a variable reference for a given object instance
* @param {JavaType} type
* @param {JavaObjectID} instance_id
* @param {string} display_format
*/
_getObjectIdReference(type, instance_id, display_format) {
// we need the type signature because we must have different id's for
// an instance and it's supertype instance (which obviously have the same objvalue)
const key = type.signature + objvalue;
// an instance and it's supertype instance (which obviously have the same instance_id)
//
// display_format is also included to give unique variable references for each display type.
// This is because VSCode caches expanded values, so once evaluated in one format, they can
// never be changed.
const key = `${type.signature}:${instance_id}:${display_format || ''}`;
let value = this.objIdCache.get(key);
if (!value) {
this.objIdCache.set(key, value = this.nextVariableRef += 1);
@@ -54,12 +62,12 @@ class VariableManager {
/**
* Convert to a VariableValue object used by VSCode
* @param {DebuggerValue} v
* @param {string} [display_format]
*/
makeVariableValue(v) {
makeVariableValue(v, display_format) {
let varref = 0;
let value = '';
const evaluateName = v.fqname || v.name;
const formats = {};
const full_typename = v.type.fullyQualifiedName();
switch(true) {
case v.hasnullvalue && JavaType.isReference(v.type):
@@ -74,93 +82,161 @@ class VariableManager {
value = v.type.typename;
break;
case v.type.signature === JavaType.String.signature:
value = JSON.stringify(v.string);
if (v.biglen) {
// since this is a big string - make it viewable on expand
varref = this._addVariable({
bigstring: v,
});
value = `String (length:${v.biglen})`;
}
else if (this._expandable_prims) {
// as a courtesy, allow strings to be expanded to see their length
varref = this._addVariable({
signature: v.type.signature,
primitive: true,
value: v.string.length
});
} else {
value = formatString(v.string, display_format);
}
break;
case JavaType.isArray(v.type):
// non-null array type - if it's not zero-length add another variable reference so the user can expand
if (v.arraylen) {
varref = this._getObjectIdReference(v.type, v.value);
varref = this._getObjectIdReference(v.type, v.value, display_format);
this._setVariable(varref, {
varref,
arrvar: v,
range:[0, v.arraylen],
display_format,
});
}
value = v.type.typename.replace(/]/, v.arraylen+']'); // insert len as the first array bound
break;
case JavaType.isClass(v.type):
// non-null object instance - add another variable reference so the user can expand
varref = this._getObjectIdReference(v.type, v.value);
varref = this._getObjectIdReference(v.type, v.value, display_format);
this._setVariable(varref, {
varref,
objvar: v,
display_format,
});
value = v.type.typename;
break;
case v.type.signature === JavaType.char.signature:
// character types have a integer value
const char = String.fromCodePoint(v.value);
const cmap = {'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t','\v':'v','\'':'\'','\\':'\\','\0':'0'};
if (cmap[char]) {
value = `'\\${cmap[char]}'`;
} else if (v.value < 32) {
value = `'\\u${v.value.toString(16).padStart(4,'0')}'`;
} else value = `'${char}'`;
value = formatChar(v.value, display_format);
break;
case v.type.signature === JavaType.long.signature:
// because JS cannot handle 64bit ints, we need a bit of extra work
const v64hex = v.value.replace(/[^0-9a-fA-F]/g,'');
value = formats.dec = NumberBaseConverter.hexToDec(v64hex, true);
formats.hex = '0x' + v64hex.replace(/^0+/, '0');
formats.oct = formats.bin = '';
// 24 bit chunks...
for (let s = v64hex; s; s = s.slice(0,-6)) {
const uint = parseInt(s.slice(-6), 16) >>> 0; // 6*4 = 24 bits
formats.oct = uint.toString(8) + formats.oct;
formats.bin = uint.toString(2) + formats.bin;
}
formats.oct = '0c' + formats.oct.replace(/^0+/, '0');
formats.bin = '0b' + formats.bin.replace(/^0+/, '0');
value = formatLong(v64hex, display_format);
break;
case JavaType.isInteger(v.type):
value = formats.dec = v.value.toString();
const uint = (v.value >>> 0);
formats.hex = '0x' + uint.toString(16);
formats.oct = '0c' + uint.toString(8);
formats.bin = '0b' + uint.toString(2);
value = formatInteger(v.value, v.type.signature, display_format);
break;
default:
// other primitives: boolean, etc
value = v.value.toString();
break;
}
// as a courtesy, allow integer and character values to be expanded to show the value in alternate bases
if (this._expandable_prims && /^[IJBSC]$/.test(v.type.signature)) {
varref = this._addVariable({
signature: v.type.signature,
primitive: true,
value: v.value,
});
}
return new VariableValue(v.name, value, full_typename, varref, evaluateName);
}
}
const cmap = {
'\b':'b','\f':'f','\r':'r','\n':'n','\t':'t',
'\v':'v','\'':'\'','\\':'\\','\0':'0'
};
function makeJavaChar(i) {
let value;
const char = String.fromCodePoint(i);
if (cmap[char]) {
value = `'\\${cmap[char]}'`;
} else if (i < 32) {
value = `'\\u${i.toString(16).padStart(4,'0')}'`;
} else value = `'${char}'`;
return value;
}
/**
* @param {number} c
* @param {string} df
*/
function formatChar(c, df) {
if (/[xX]b|o|bb|d/.test(df)) {
return formatInteger(c, 'C', df);
}
return makeJavaChar(c);
}
/**
*
* @param {string} s
* @param {string} display_format
*/
function formatString(s, display_format) {
if (display_format === '!') {
return s;
}
let value = JSON.stringify(s);
if (display_format === 'sb') {
// remove quotes
value = value.slice(1,-1);
}
return value;
}
/**
* @param {hex64} hex64
* @param {string} df
*/
function formatLong(hex64, df) {
let minlength;
if (/[xX]b?/.test(df)) {
minlength = Math.ceil(64 / 4);
let s = `${df[1]?'':'0x'}${hex64.padStart(minlength,'0')}`;
return df[0] === 'x' ? s.toLowerCase() : s.toUpperCase();
}
if (/o/.test(df)) {
minlength = Math.ceil(64 / 3);
return `${df[1]?'':'0'}${NumberBaseConverter.convertBase(hex64,16,8).padStart(minlength, '0')}`;
}
if (/bb?/.test(df)) {
minlength = 64;
return `${df[1]?'':'0b'}${NumberBaseConverter.convertBase(hex64,16,2).padStart(minlength, '0')}`;
}
if (/c/.test(df)) {
return makeJavaChar(parseInt(hex64.slice(-4), 16));
}
return NumberBaseConverter.convertBase(hex64, 16, 10);
}
/**
* @param {number} i
* @param {string} signature
* @param {string} df
*/
function formatInteger(i, signature, df) {
const bits = { B:8,S:16,I:32,C:16 }[signature];
let u = (i & (-1 >>> (32 - bits))) >>> 0;
let minlength;
if (/[xX]b?/.test(df)) {
minlength = Math.ceil(bits / 4);
let s = u.toString(16).padStart(minlength,'0');
s = df[0] === 'x' ? s.toLowerCase() : s.toUpperCase();
return `${df[1]?'':'0x'}${s}`;
}
if (/o/.test(df)) {
minlength = Math.ceil(bits / 3);
return `${df[1]?'':'0'}${u.toString(8).padStart(minlength, '0')}`;
}
if (/bb?/.test(df)) {
minlength = bits;
return `${df[1]?'':'0b'}${u.toString(2).padStart(minlength, '0')}`;
}
if (/c/.test(df)) {
minlength = bits;
return makeJavaChar(u & 0xffff);
}
return i.toString();
}
module.exports = {
VariableManager,
}