From 7842f9a715f143e5bae2e3886148379279976bbc Mon Sep 17 00:00:00 2001 From: smurf3tte <75271109+smurf3tte@users.noreply.github.com> Date: Wed, 16 Dec 2020 15:40:20 -0800 Subject: [PATCH 1/7] Debugger: Initial implementation of conditional breakpoints Expression class to store compiled expressions and associated variable list. Co-authored-by: TryTwo --- CMakeLists.txt | 2 + Externals/expr/CMakeLists.txt | 2 + Externals/expr/LICENSE | 21 + Externals/expr/include/expr.h | 921 ++++++++++++++++++ Externals/licenses.md | 2 + Source/Core/Core/CMakeLists.txt | 3 + Source/Core/Core/PowerPC/BreakPoints.cpp | 56 +- Source/Core/Core/PowerPC/BreakPoints.h | 11 +- Source/Core/Core/PowerPC/Expression.cpp | 130 +++ Source/Core/Core/PowerPC/Expression.h | 73 ++ Source/Core/Core/PowerPC/PowerPC.cpp | 8 +- Source/Core/DolphinLib.props | 2 + .../DolphinQt/Debugger/BreakpointWidget.cpp | 21 +- .../DolphinQt/Debugger/BreakpointWidget.h | 2 +- .../Debugger/NewBreakpointDialog.cpp | 20 +- .../DolphinQt/Debugger/NewBreakpointDialog.h | 1 + Source/VSProps/Base.Dolphin.props | 1 + 17 files changed, 1240 insertions(+), 36 deletions(-) create mode 100644 Externals/expr/CMakeLists.txt create mode 100644 Externals/expr/LICENSE create mode 100644 Externals/expr/include/expr.h create mode 100644 Source/Core/Core/PowerPC/Expression.cpp create mode 100644 Source/Core/Core/PowerPC/Expression.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 04bcdde4c2..799ddcb238 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -967,6 +967,8 @@ endif() include_directories(Externals/picojson) +add_subdirectory(Externals/expr) + add_subdirectory(Externals/rangeset) add_subdirectory(Externals/FatFs) diff --git a/Externals/expr/CMakeLists.txt b/Externals/expr/CMakeLists.txt new file mode 100644 index 0000000000..4eb00fa0e9 --- /dev/null +++ b/Externals/expr/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(expr INTERFACE) +target_include_directories(expr INTERFACE include/) diff --git a/Externals/expr/LICENSE b/Externals/expr/LICENSE new file mode 100644 index 0000000000..44113db6d8 --- /dev/null +++ b/Externals/expr/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Serge Zaitsev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Externals/expr/include/expr.h b/Externals/expr/include/expr.h new file mode 100644 index 0000000000..59ba4dbaad --- /dev/null +++ b/Externals/expr/include/expr.h @@ -0,0 +1,921 @@ +#ifndef EXPR_H +#define EXPR_H + +#ifdef _MSC_VER +#pragma warning(push) +// Disable warning for zero-sized array: +#pragma warning(disable : 4200) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#include /* for isspace */ +#include +#include /* for pow */ +#include +#include +#include + +/* + * Simple expandable vector implementation + */ +static int vec_expand(char **buf, int *length, int *cap, int memsz) { + if (*length + 1 > *cap) { + void *ptr; + int n = (*cap == 0) ? 1 : *cap << 1; + ptr = realloc(*buf, n * memsz); + if (ptr == NULL) { + return -1; /* allocation failed */ + } + *buf = (char *)ptr; + *cap = n; + } + return 0; +} +#define vec(T) \ + struct { \ + T *buf; \ + int len; \ + int cap; \ + } +#define vec_init() \ + { NULL, 0, 0 } +#define vec_len(v) ((v)->len) +#define vec_unpack(v) \ + (char **)&(v)->buf, &(v)->len, &(v)->cap, sizeof(*(v)->buf) +#define vec_push(v, val) \ + vec_expand(vec_unpack(v)) ? -1 : ((v)->buf[(v)->len++] = (val), 0) +#define vec_nth(v, i) (v)->buf[i] +#define vec_peek(v) (v)->buf[(v)->len - 1] +#define vec_pop(v) (v)->buf[--(v)->len] +#define vec_free(v) (free((v)->buf), (v)->buf = NULL, (v)->len = (v)->cap = 0) +#define vec_foreach(v, var, iter) \ + if ((v)->len > 0) \ + for ((iter) = 0; (iter) < (v)->len && (((var) = (v)->buf[(iter)]), 1); \ + ++(iter)) + +/* + * Expression data types + */ +struct expr; +struct expr_func; + +enum expr_type { + OP_UNKNOWN, + OP_UNARY_MINUS, + OP_UNARY_LOGICAL_NOT, + OP_UNARY_BITWISE_NOT, + + OP_POWER, + OP_DIVIDE, + OP_MULTIPLY, + OP_REMAINDER, + + OP_PLUS, + OP_MINUS, + + OP_SHL, + OP_SHR, + + OP_LT, + OP_LE, + OP_GT, + OP_GE, + OP_EQ, + OP_NE, + + OP_BITWISE_AND, + OP_BITWISE_OR, + OP_BITWISE_XOR, + + OP_LOGICAL_AND, + OP_LOGICAL_OR, + + OP_ASSIGN, + OP_COMMA, + + OP_CONST, + OP_VAR, + OP_FUNC, +}; + +static int prec[] = {0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 5, + 5, 5, 5, 5, 6, 7, 8, 9, 10, 11, 12, 0, 0, 0}; + +typedef vec(struct expr) vec_expr_t; +typedef void (*exprfn_cleanup_t)(struct expr_func *f, void *context); +typedef double (*exprfn_t)(struct expr_func *f, vec_expr_t *args, void *context); + +struct expr { + enum expr_type type; + union { + struct { + double value; + } num; + struct { + double *value; + } var; + struct { + vec_expr_t args; + } op; + struct { + struct expr_func *f; + vec_expr_t args; + void *context; + } func; + } param; +}; + +#define expr_init() \ + { (enum expr_type)0 } + +struct expr_string { + const char *s; + int n; +}; +struct expr_arg { + int oslen; + int eslen; + vec_expr_t args; +}; + +typedef vec(struct expr_string) vec_str_t; +typedef vec(struct expr_arg) vec_arg_t; + +static int expr_is_unary(enum expr_type op) { + return op == OP_UNARY_MINUS || op == OP_UNARY_LOGICAL_NOT || + op == OP_UNARY_BITWISE_NOT; +} + +static int expr_is_binary(enum expr_type op) { + return !expr_is_unary(op) && op != OP_CONST && op != OP_VAR && + op != OP_FUNC && op != OP_UNKNOWN; +} + +static int expr_prec(enum expr_type a, enum expr_type b) { + int left = + expr_is_binary(a) && a != OP_ASSIGN && a != OP_POWER && a != OP_COMMA; + return (left && prec[a] >= prec[b]) || (prec[a] > prec[b]); +} + +#define isfirstvarchr(c) \ + (((unsigned char)c >= '@' && c != '^' && c != '|') || c == '$') +#define isvarchr(c) \ + (((unsigned char)c >= '@' && c != '^' && c != '|') || c == '$' || \ + c == '#' || (c >= '0' && c <= '9')) + +static struct { + const char *s; + const enum expr_type op; +} OPS[] = { + {"-u", OP_UNARY_MINUS}, + {"!u", OP_UNARY_LOGICAL_NOT}, + {"^u", OP_UNARY_BITWISE_NOT}, + {"**", OP_POWER}, + {"*", OP_MULTIPLY}, + {"/", OP_DIVIDE}, + {"%", OP_REMAINDER}, + {"+", OP_PLUS}, + {"-", OP_MINUS}, + {"<<", OP_SHL}, + {">>", OP_SHR}, + {"<", OP_LT}, + {"<=", OP_LE}, + {">", OP_GT}, + {">=", OP_GE}, + {"==", OP_EQ}, + {"!=", OP_NE}, + {"&", OP_BITWISE_AND}, + {"|", OP_BITWISE_OR}, + {"^", OP_BITWISE_XOR}, + {"&&", OP_LOGICAL_AND}, + {"||", OP_LOGICAL_OR}, + {"=", OP_ASSIGN}, + {",", OP_COMMA}, + + /* These are used by lexer and must be ignored by parser, so we put + them at the end */ + {"-", OP_UNARY_MINUS}, + {"!", OP_UNARY_LOGICAL_NOT}, + {"^", OP_UNARY_BITWISE_NOT}, +}; + +static enum expr_type expr_op(const char *s, size_t len, int unary) { + for (unsigned int i = 0; i < sizeof(OPS) / sizeof(OPS[0]); i++) { + if (strlen(OPS[i].s) == len && strncmp(OPS[i].s, s, len) == 0 && + (unary == -1 || expr_is_unary(OPS[i].op) == unary)) { + return OPS[i].op; + } + } + return OP_UNKNOWN; +} + +static double expr_parse_number(const char *s, size_t len) { + double num = 0; + unsigned int frac = 0; + unsigned int digits = 0; + for (unsigned int i = 0; i < len; i++) { + if (s[i] == '.' && frac == 0) { + frac++; + continue; + } + if (isdigit(s[i])) { + digits++; + if (frac > 0) { + frac++; + } + num = num * 10 + (s[i] - '0'); + } else { + return NAN; + } + } + while (frac > 1) { + num = num / 10; + frac--; + } + return (digits > 0 ? num : NAN); +} + +/* + * Functions + */ +struct expr_func { + const char *name; + exprfn_t f; + exprfn_cleanup_t cleanup; + size_t ctxsz; +}; + +static struct expr_func *expr_get_func(struct expr_func *funcs, const char *s, + size_t len) { + for (struct expr_func *f = funcs; f->name; f++) { + if (strlen(f->name) == len && strncmp(f->name, s, len) == 0) { + return f; + } + } + return NULL; +} + +/* + * Variables + */ +struct expr_var { + double value; + struct expr_var *next; + char name[]; +}; + +struct expr_var_list { + struct expr_var *head; +}; + +static struct expr_var *expr_get_var(struct expr_var_list *vars, const char *s, + size_t len) { + struct expr_var *v = NULL; + if (len == 0 || !isfirstvarchr(*s)) { + return NULL; + } + for (v = vars->head; v; v = v->next) { + if (strlen(v->name) == len && strncmp(v->name, s, len) == 0) { + return v; + } + } + v = (struct expr_var *)calloc(1, sizeof(struct expr_var) + len + 1); + if (v == NULL) { + return NULL; /* allocation failed */ + } + v->next = vars->head; + v->value = 0; + strncpy(v->name, s, len); + v->name[len] = '\0'; + vars->head = v; + return v; +} + +static int to_int(double x) { + if (isnan(x)) { + return 0; + } else if (isinf(x) != 0) { + return INT_MAX * isinf(x); + } else { + return (int)x; + } +} + +static double expr_eval(struct expr *e) { + double n; + switch (e->type) { + case OP_UNARY_MINUS: + return -(expr_eval(&e->param.op.args.buf[0])); + case OP_UNARY_LOGICAL_NOT: + return !(expr_eval(&e->param.op.args.buf[0])); + case OP_UNARY_BITWISE_NOT: + return ~(to_int(expr_eval(&e->param.op.args.buf[0]))); + case OP_POWER: + return pow(expr_eval(&e->param.op.args.buf[0]), + expr_eval(&e->param.op.args.buf[1])); + case OP_MULTIPLY: + return expr_eval(&e->param.op.args.buf[0]) * + expr_eval(&e->param.op.args.buf[1]); + case OP_DIVIDE: + return expr_eval(&e->param.op.args.buf[0]) / + expr_eval(&e->param.op.args.buf[1]); + case OP_REMAINDER: + return fmod(expr_eval(&e->param.op.args.buf[0]), + expr_eval(&e->param.op.args.buf[1])); + case OP_PLUS: + return expr_eval(&e->param.op.args.buf[0]) + + expr_eval(&e->param.op.args.buf[1]); + case OP_MINUS: + return expr_eval(&e->param.op.args.buf[0]) - + expr_eval(&e->param.op.args.buf[1]); + case OP_SHL: + return to_int(expr_eval(&e->param.op.args.buf[0])) + << to_int(expr_eval(&e->param.op.args.buf[1])); + case OP_SHR: + return to_int(expr_eval(&e->param.op.args.buf[0])) >> + to_int(expr_eval(&e->param.op.args.buf[1])); + case OP_LT: + return expr_eval(&e->param.op.args.buf[0]) < + expr_eval(&e->param.op.args.buf[1]); + case OP_LE: + return expr_eval(&e->param.op.args.buf[0]) <= + expr_eval(&e->param.op.args.buf[1]); + case OP_GT: + return expr_eval(&e->param.op.args.buf[0]) > + expr_eval(&e->param.op.args.buf[1]); + case OP_GE: + return expr_eval(&e->param.op.args.buf[0]) >= + expr_eval(&e->param.op.args.buf[1]); + case OP_EQ: + return expr_eval(&e->param.op.args.buf[0]) == + expr_eval(&e->param.op.args.buf[1]); + case OP_NE: + return expr_eval(&e->param.op.args.buf[0]) != + expr_eval(&e->param.op.args.buf[1]); + case OP_BITWISE_AND: + return to_int(expr_eval(&e->param.op.args.buf[0])) & + to_int(expr_eval(&e->param.op.args.buf[1])); + case OP_BITWISE_OR: + return to_int(expr_eval(&e->param.op.args.buf[0])) | + to_int(expr_eval(&e->param.op.args.buf[1])); + case OP_BITWISE_XOR: + return to_int(expr_eval(&e->param.op.args.buf[0])) ^ + to_int(expr_eval(&e->param.op.args.buf[1])); + case OP_LOGICAL_AND: + n = expr_eval(&e->param.op.args.buf[0]); + if (n != 0) { + n = expr_eval(&e->param.op.args.buf[1]); + if (n != 0) { + return n; + } + } + return 0; + case OP_LOGICAL_OR: + n = expr_eval(&e->param.op.args.buf[0]); + if (n != 0 && !isnan(n)) { + return n; + } else { + n = expr_eval(&e->param.op.args.buf[1]); + if (n != 0) { + return n; + } + } + return 0; + case OP_ASSIGN: + n = expr_eval(&e->param.op.args.buf[1]); + if (vec_nth(&e->param.op.args, 0).type == OP_VAR) { + *e->param.op.args.buf[0].param.var.value = n; + } + return n; + case OP_COMMA: + expr_eval(&e->param.op.args.buf[0]); + return expr_eval(&e->param.op.args.buf[1]); + case OP_CONST: + return e->param.num.value; + case OP_VAR: + return *e->param.var.value; + case OP_FUNC: + return e->param.func.f->f(e->param.func.f, &e->param.func.args, + e->param.func.context); + default: + return NAN; + } +} + +#define EXPR_TOP (1 << 0) +#define EXPR_TOPEN (1 << 1) +#define EXPR_TCLOSE (1 << 2) +#define EXPR_TNUMBER (1 << 3) +#define EXPR_TWORD (1 << 4) +#define EXPR_TDEFAULT (EXPR_TOPEN | EXPR_TNUMBER | EXPR_TWORD) + +#define EXPR_UNARY (1 << 5) +#define EXPR_COMMA (1 << 6) + +static int expr_next_token(const char *s, size_t len, int *flags) { + unsigned int i = 0; + if (len == 0) { + return 0; + } + char c = s[0]; + if (c == '#') { + for (; i < len && s[i] != '\n'; i++) + ; + return i; + } else if (c == '\n') { + for (; i < len && isspace(s[i]); i++) + ; + if (*flags & EXPR_TOP) { + if (i == len || s[i] == ')') { + *flags = *flags & (~EXPR_COMMA); + } else { + *flags = EXPR_TNUMBER | EXPR_TWORD | EXPR_TOPEN | EXPR_COMMA; + } + } + return i; + } else if (isspace(c)) { + while (i < len && isspace(s[i]) && s[i] != '\n') { + i++; + } + return i; + } else if (isdigit(c)) { + if ((*flags & EXPR_TNUMBER) == 0) { + return -1; // unexpected number + } + *flags = EXPR_TOP | EXPR_TCLOSE; + while ((c == '.' || isdigit(c)) && i < len) { + i++; + c = s[i]; + } + return i; + } else if (isfirstvarchr(c)) { + if ((*flags & EXPR_TWORD) == 0) { + return -2; // unexpected word + } + *flags = EXPR_TOP | EXPR_TOPEN | EXPR_TCLOSE; + while ((isvarchr(c)) && i < len) { + i++; + c = s[i]; + } + return i; + } else if (c == '(' || c == ')') { + if (c == '(' && (*flags & EXPR_TOPEN) != 0) { + *flags = EXPR_TNUMBER | EXPR_TWORD | EXPR_TOPEN | EXPR_TCLOSE; + } else if (c == ')' && (*flags & EXPR_TCLOSE) != 0) { + *flags = EXPR_TOP | EXPR_TCLOSE; + } else { + return -3; // unexpected parenthesis + } + return 1; + } else { + if ((*flags & EXPR_TOP) == 0) { + if (expr_op(&c, 1, 1) == OP_UNKNOWN) { + return -4; // missing expected operand + } + *flags = EXPR_TNUMBER | EXPR_TWORD | EXPR_TOPEN | EXPR_UNARY; + return 1; + } else { + int found = 0; + while (!isvarchr(c) && !isspace(c) && c != '(' && c != ')' && i < len) { + if (expr_op(s, i + 1, 0) != OP_UNKNOWN) { + found = 1; + } else if (found) { + break; + } + i++; + c = s[i]; + } + if (!found) { + return -5; // unknown operator + } + *flags = EXPR_TNUMBER | EXPR_TWORD | EXPR_TOPEN; + return i; + } + } +} + +#define EXPR_PAREN_ALLOWED 0 +#define EXPR_PAREN_EXPECTED 1 +#define EXPR_PAREN_FORBIDDEN 2 + +static int expr_bind(const char *s, size_t len, vec_expr_t *es) { + enum expr_type op = expr_op(s, len, -1); + if (op == OP_UNKNOWN) { + return -1; + } + + if (expr_is_unary(op)) { + if (vec_len(es) < 1) { + return -1; + } + struct expr arg = vec_pop(es); + struct expr unary = expr_init(); + unary.type = op; + vec_push(&unary.param.op.args, arg); + vec_push(es, unary); + } else { + if (vec_len(es) < 2) { + return -1; + } + struct expr b = vec_pop(es); + struct expr a = vec_pop(es); + struct expr binary = expr_init(); + binary.type = op; + if (op == OP_ASSIGN && a.type != OP_VAR) { + return -1; /* Bad assignment */ + } + vec_push(&binary.param.op.args, a); + vec_push(&binary.param.op.args, b); + vec_push(es, binary); + } + return 0; +} + +static struct expr expr_const(double value) { + struct expr e = expr_init(); + e.type = OP_CONST; + e.param.num.value = value; + return e; +} + +static struct expr expr_varref(struct expr_var *v) { + struct expr e = expr_init(); + e.type = OP_VAR; + e.param.var.value = &v->value; + return e; +} + +static struct expr expr_binary(enum expr_type type, struct expr a, + struct expr b) { + struct expr e = expr_init(); + e.type = type; + vec_push(&e.param.op.args, a); + vec_push(&e.param.op.args, b); + return e; +} + +static inline void expr_copy(struct expr *dst, struct expr *src) { + int i; + struct expr arg; + dst->type = src->type; + if (src->type == OP_FUNC) { + dst->param.func.f = src->param.func.f; + vec_foreach(&src->param.func.args, arg, i) { + struct expr tmp = expr_init(); + expr_copy(&tmp, &arg); + vec_push(&dst->param.func.args, tmp); + } + if (src->param.func.f->ctxsz > 0) { + dst->param.func.context = calloc(1, src->param.func.f->ctxsz); + } + } else if (src->type == OP_CONST) { + dst->param.num.value = src->param.num.value; + } else if (src->type == OP_VAR) { + dst->param.var.value = src->param.var.value; + } else { + vec_foreach(&src->param.op.args, arg, i) { + struct expr tmp = expr_init(); + expr_copy(&tmp, &arg); + vec_push(&dst->param.op.args, tmp); + } + } +} + +static void expr_destroy_args(struct expr *e); + +static struct expr *expr_create(const char *s, size_t len, + struct expr_var_list *vars, + struct expr_func *funcs) { + double num; + const char *id = NULL; + size_t idn = 0; + + struct expr *result = NULL; + + vec_expr_t es = vec_init(); + vec_str_t os = vec_init(); + vec_arg_t as = vec_init(); + + struct macro { + char *name; + vec_expr_t body; + }; + vec(struct macro) macros = vec_init(); + + int flags = EXPR_TDEFAULT; + int paren = EXPR_PAREN_ALLOWED; + for (;;) { + int n = expr_next_token(s, len, &flags); + if (n == 0) { + break; + } else if (n < 0) { + goto cleanup; + } + const char *tok = s; + s = s + n; + len = len - n; + if (*tok == '#') { + continue; + } + if (flags & EXPR_UNARY) { + if (n == 1) { + switch (*tok) { + case '-': + tok = "-u"; + break; + case '^': + tok = "^u"; + break; + case '!': + tok = "!u"; + break; + default: + goto cleanup; + } + n = 2; + } + } + if (*tok == '\n' && (flags & EXPR_COMMA)) { + flags = flags & (~EXPR_COMMA); + n = 1; + tok = ","; + } + if (isspace(*tok)) { + continue; + } + int paren_next = EXPR_PAREN_ALLOWED; + + if (idn > 0) { + struct expr_var *v; + if (n == 1 && *tok == '(') { + int i; + int has_macro = 0; + struct macro m; + vec_foreach(¯os, m, i) { + if (strlen(m.name) == idn && strncmp(m.name, id, idn) == 0) { + has_macro = 1; + break; + } + } + if ((idn == 1 && id[0] == '$') || has_macro || + expr_get_func(funcs, id, idn) != NULL) { + struct expr_string str = {id, (int)idn}; + vec_push(&os, str); + paren = EXPR_PAREN_EXPECTED; + } else { + goto cleanup; /* invalid function name */ + } + } else if ((v = expr_get_var(vars, id, idn)) != NULL) { + vec_push(&es, expr_varref(v)); + paren = EXPR_PAREN_FORBIDDEN; + } + id = NULL; + idn = 0; + } + + if (n == 1 && *tok == '(') { + if (paren == EXPR_PAREN_EXPECTED) { + struct expr_string str = {"{", 1}; + vec_push(&os, str); + struct expr_arg arg = {vec_len(&os), vec_len(&es), vec_init()}; + vec_push(&as, arg); + } else if (paren == EXPR_PAREN_ALLOWED) { + struct expr_string str = {"(", 1}; + vec_push(&os, str); + } else { + goto cleanup; // Bad call + } + } else if (paren == EXPR_PAREN_EXPECTED) { + goto cleanup; // Bad call + } else if (n == 1 && *tok == ')') { + int minlen = (vec_len(&as) > 0 ? vec_peek(&as).oslen : 0); + while (vec_len(&os) > minlen && *vec_peek(&os).s != '(' && + *vec_peek(&os).s != '{') { + struct expr_string str = vec_pop(&os); + if (expr_bind(str.s, str.n, &es) == -1) { + goto cleanup; + } + } + if (vec_len(&os) == 0) { + goto cleanup; // Bad parens + } + struct expr_string str = vec_pop(&os); + if (str.n == 1 && *str.s == '{') { + str = vec_pop(&os); + struct expr_arg arg = vec_pop(&as); + if (vec_len(&es) > arg.eslen) { + vec_push(&arg.args, vec_pop(&es)); + } + if (str.n == 1 && str.s[0] == '$') { + if (vec_len(&arg.args) < 1) { + vec_free(&arg.args); + goto cleanup; /* too few arguments for $() function */ + } + struct expr *u = &vec_nth(&arg.args, 0); + if (u->type != OP_VAR) { + vec_free(&arg.args); + goto cleanup; /* first argument is not a variable */ + } + for (struct expr_var *v = vars->head; v; v = v->next) { + if (&v->value == u->param.var.value) { + struct macro m = {v->name, arg.args}; + vec_push(¯os, m); + break; + } + } + vec_push(&es, expr_const(0)); + } else { + int i = 0; + int found = -1; + struct macro m; + vec_foreach(¯os, m, i) { + if (strlen(m.name) == (size_t)str.n && + strncmp(m.name, str.s, str.n) == 0) { + found = i; + } + } + if (found != -1) { + m = vec_nth(¯os, found); + struct expr root = expr_const(0); + struct expr *p = &root; + /* Assign macro parameters */ + for (int j = 0; j < vec_len(&arg.args); j++) { + char varname[12]; + snprintf(varname, sizeof(varname), "$%d", (j + 1)); + struct expr_var *v = expr_get_var(vars, varname, strlen(varname)); + struct expr ev = expr_varref(v); + struct expr assign = + expr_binary(OP_ASSIGN, ev, vec_nth(&arg.args, j)); + *p = expr_binary(OP_COMMA, assign, expr_const(0)); + p = &vec_nth(&p->param.op.args, 1); + } + /* Expand macro body */ + for (int j = 1; j < vec_len(&m.body); j++) { + if (j < vec_len(&m.body) - 1) { + *p = expr_binary(OP_COMMA, expr_const(0), expr_const(0)); + expr_copy(&vec_nth(&p->param.op.args, 0), &vec_nth(&m.body, j)); + } else { + expr_copy(p, &vec_nth(&m.body, j)); + } + p = &vec_nth(&p->param.op.args, 1); + } + vec_push(&es, root); + vec_free(&arg.args); + } else { + struct expr_func *f = expr_get_func(funcs, str.s, str.n); + struct expr bound_func = expr_init(); + bound_func.type = OP_FUNC; + bound_func.param.func.f = f; + bound_func.param.func.args = arg.args; + if (f->ctxsz > 0) { + void *p = calloc(1, f->ctxsz); + if (p == NULL) { + goto cleanup; /* allocation failed */ + } + bound_func.param.func.context = p; + } + vec_push(&es, bound_func); + } + } + } + paren_next = EXPR_PAREN_FORBIDDEN; + } else if (!isnan(num = expr_parse_number(tok, n))) { + vec_push(&es, expr_const(num)); + paren_next = EXPR_PAREN_FORBIDDEN; + } else if (expr_op(tok, n, -1) != OP_UNKNOWN) { + enum expr_type op = expr_op(tok, n, -1); + struct expr_string o2 = {NULL, 0}; + if (vec_len(&os) > 0) { + o2 = vec_peek(&os); + } + for (;;) { + if (n == 1 && *tok == ',' && vec_len(&os) > 0) { + struct expr_string str = vec_peek(&os); + if (str.n == 1 && *str.s == '{') { + struct expr e = vec_pop(&es); + vec_push(&vec_peek(&as).args, e); + break; + } + } + enum expr_type type2 = expr_op(o2.s, o2.n, -1); + if (!(type2 != OP_UNKNOWN && expr_prec(op, type2))) { + struct expr_string str = {tok, n}; + vec_push(&os, str); + break; + } + + if (expr_bind(o2.s, o2.n, &es) == -1) { + goto cleanup; + } + (void)vec_pop(&os); + if (vec_len(&os) > 0) { + o2 = vec_peek(&os); + } else { + o2.n = 0; + } + } + } else { + if (n > 0 && !isdigit(*tok)) { + /* Valid identifier, a variable or a function */ + id = tok; + idn = n; + } else { + goto cleanup; // Bad variable name, e.g. '2.3.4' or '4ever' + } + } + paren = paren_next; + } + + if (idn > 0) { + vec_push(&es, expr_varref(expr_get_var(vars, id, idn))); + } + + while (vec_len(&os) > 0) { + struct expr_string rest = vec_pop(&os); + if (rest.n == 1 && (*rest.s == '(' || *rest.s == ')')) { + goto cleanup; // Bad paren + } + if (expr_bind(rest.s, rest.n, &es) == -1) { + goto cleanup; + } + } + + result = (struct expr *)calloc(1, sizeof(struct expr)); + if (result != NULL) { + if (vec_len(&es) == 0) { + result->type = OP_CONST; + } else { + *result = vec_pop(&es); + } + } + + int i, j; + struct macro m; + struct expr e; + struct expr_arg a; +cleanup: + vec_foreach(¯os, m, i) { + struct expr e2; + vec_foreach(&m.body, e2, j) { expr_destroy_args(&e2); } + vec_free(&m.body); + } + vec_free(¯os); + + vec_foreach(&es, e, i) { expr_destroy_args(&e); } + vec_free(&es); + + vec_foreach(&as, a, i) { + vec_foreach(&a.args, e, j) { expr_destroy_args(&e); } + vec_free(&a.args); + } + vec_free(&as); + + /*vec_foreach(&os, o, i) {vec_free(&m.body);}*/ + vec_free(&os); + return result; +} + +static void expr_destroy_args(struct expr *e) { + int i; + struct expr arg; + if (e->type == OP_FUNC) { + vec_foreach(&e->param.func.args, arg, i) { expr_destroy_args(&arg); } + vec_free(&e->param.func.args); + if (e->param.func.context != NULL) { + if (e->param.func.f->cleanup != NULL) { + e->param.func.f->cleanup(e->param.func.f, e->param.func.context); + } + free(e->param.func.context); + } + } else if (e->type != OP_CONST && e->type != OP_VAR) { + vec_foreach(&e->param.op.args, arg, i) { expr_destroy_args(&arg); } + vec_free(&e->param.op.args); + } +} + +static void expr_destroy(struct expr *e, struct expr_var_list *vars) { + if (e != NULL) { + expr_destroy_args(e); + free(e); + } + if (vars != NULL) { + for (struct expr_var *v = vars->head; v;) { + struct expr_var *next = v->next; + free(v); + v = next; + } + } +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +#endif /* EXPR_H */ diff --git a/Externals/licenses.md b/Externals/licenses.md index 9ae0318ca6..088aaffc13 100644 --- a/Externals/licenses.md +++ b/Externals/licenses.md @@ -14,6 +14,8 @@ Dolphin includes or links code of the following third-party software projects: [MIT](https://github.com/discordapp/discord-rpc/blob/master/LICENSE) - [ENet](http://enet.bespin.org/): [MIT](http://enet.bespin.org/License.html) +- [expr](https://github.com/zserge/expr): + [MIT](https://github.com/zserge/expr/blob/master/LICENSE) - [FatFs](http://elm-chan.org/fsw/ff/00index_e.html): [BSD 1-Clause](http://elm-chan.org/fsw/ff/doc/appnote.html#license) - [GCEmu](http://sourceforge.net/projects/gcemu-project/): diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index 26787fcd3d..fd90b9f4fb 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -438,6 +438,8 @@ add_library(core PowerPC/CachedInterpreter/InterpreterBlockCache.h PowerPC/ConditionRegister.cpp PowerPC/ConditionRegister.h + PowerPC/Expression.cpp + PowerPC/Expression.h PowerPC/Interpreter/ExceptionUtils.h PowerPC/Interpreter/Interpreter_Branch.cpp PowerPC/Interpreter/Interpreter_FloatingPoint.cpp @@ -582,6 +584,7 @@ PUBLIC cubeb discio enet + expr inputcommon ${MBEDTLS_LIBRARIES} pugixml diff --git a/Source/Core/Core/PowerPC/BreakPoints.cpp b/Source/Core/Core/PowerPC/BreakPoints.cpp index 9690d11c89..c900a1c98d 100644 --- a/Source/Core/Core/PowerPC/BreakPoints.cpp +++ b/Source/Core/Core/PowerPC/BreakPoints.cpp @@ -13,6 +13,7 @@ #include "Common/DebugInterface.h" #include "Common/Logging/Log.h" #include "Core/Core.h" +#include "Core/PowerPC/Expression.h" #include "Core/PowerPC/JitInterface.h" #include "Core/PowerPC/MMU.h" @@ -35,17 +36,16 @@ bool BreakPoints::IsTempBreakPoint(u32 address) const }); } -bool BreakPoints::IsBreakPointBreakOnHit(u32 address) const +const TBreakPoint* BreakPoints::GetBreakpoint(u32 address) const { - return std::any_of(m_breakpoints.begin(), m_breakpoints.end(), [address](const auto& bp) { - return bp.address == address && bp.break_on_hit; + auto bp = std::find_if(m_breakpoints.begin(), m_breakpoints.end(), [address](const auto& bp) { + return bp.is_enabled && bp.address == address; }); -} -bool BreakPoints::IsBreakPointLogOnHit(u32 address) const -{ - return std::any_of(m_breakpoints.begin(), m_breakpoints.end(), - [address](const auto& bp) { return bp.address == address && bp.log_on_hit; }); + if (bp == m_breakpoints.end() || !EvaluateCondition(bp->condition)) + return nullptr; + + return &*bp; } BreakPoints::TBreakPointsStr BreakPoints::GetStrings() const @@ -57,10 +57,16 @@ BreakPoints::TBreakPointsStr BreakPoints::GetStrings() const { std::ostringstream ss; ss.imbue(std::locale::classic()); - - ss << std::hex << bp.address << " " << (bp.is_enabled ? "n" : "") - << (bp.log_on_hit ? "l" : "") << (bp.break_on_hit ? "b" : ""); - bp_strings.push_back(ss.str()); + ss << fmt::format("${:08x} ", bp.address); + if (bp.is_enabled) + ss << "n"; + if (bp.log_on_hit) + ss << "l"; + if (bp.break_on_hit) + ss << "b"; + if (bp.condition) + ss << "c " << bp.condition->GetText(); + bp_strings.emplace_back(ss.str()); } } @@ -76,32 +82,43 @@ void BreakPoints::AddFromStrings(const TBreakPointsStr& bp_strings) std::istringstream iss(bp_string); iss.imbue(std::locale::classic()); + if (iss.peek() == '$') + iss.ignore(); + iss >> std::hex >> bp.address; iss >> flags; bp.is_enabled = flags.find('n') != flags.npos; bp.log_on_hit = flags.find('l') != flags.npos; bp.break_on_hit = flags.find('b') != flags.npos; + if (flags.find('c') != std::string::npos) + { + iss >> std::ws; + std::string condition; + std::getline(iss, condition); + bp.condition = Expression::TryParse(condition); + } bp.is_temporary = false; - Add(bp); + Add(std::move(bp)); } } -void BreakPoints::Add(const TBreakPoint& bp) +void BreakPoints::Add(TBreakPoint bp) { if (IsAddressBreakPoint(bp.address)) return; - m_breakpoints.push_back(bp); - JitInterface::InvalidateICache(bp.address, 4, true); + + m_breakpoints.emplace_back(std::move(bp)); } void BreakPoints::Add(u32 address, bool temp) { - BreakPoints::Add(address, temp, true, false); + BreakPoints::Add(address, temp, true, false, std::nullopt); } -void BreakPoints::Add(u32 address, bool temp, bool break_on_hit, bool log_on_hit) +void BreakPoints::Add(u32 address, bool temp, bool break_on_hit, bool log_on_hit, + std::optional condition) { // Only add new addresses if (IsAddressBreakPoint(address)) @@ -113,8 +130,9 @@ void BreakPoints::Add(u32 address, bool temp, bool break_on_hit, bool log_on_hit bp.break_on_hit = break_on_hit; bp.log_on_hit = log_on_hit; bp.address = address; + bp.condition = std::move(condition); - m_breakpoints.push_back(bp); + m_breakpoints.emplace_back(std::move(bp)); JitInterface::InvalidateICache(address, 4, true); } diff --git a/Source/Core/Core/PowerPC/BreakPoints.h b/Source/Core/Core/PowerPC/BreakPoints.h index 5fedd8f807..5a8a74b9f7 100644 --- a/Source/Core/Core/PowerPC/BreakPoints.h +++ b/Source/Core/Core/PowerPC/BreakPoints.h @@ -4,10 +4,12 @@ #pragma once #include +#include #include #include #include "Common/CommonTypes.h" +#include "Core/PowerPC/Expression.h" namespace Common { @@ -21,6 +23,7 @@ struct TBreakPoint bool is_temporary = false; bool log_on_hit = false; bool break_on_hit = false; + std::optional condition; }; struct TMemCheck @@ -59,13 +62,13 @@ public: bool IsAddressBreakPoint(u32 address) const; bool IsBreakPointEnable(u32 adresss) const; bool IsTempBreakPoint(u32 address) const; - bool IsBreakPointBreakOnHit(u32 address) const; - bool IsBreakPointLogOnHit(u32 address) const; + const TBreakPoint* GetBreakpoint(u32 address) const; // Add BreakPoint - void Add(u32 address, bool temp, bool break_on_hit, bool log_on_hit); + void Add(u32 address, bool temp, bool break_on_hit, bool log_on_hit, + std::optional condition); void Add(u32 address, bool temp = false); - void Add(const TBreakPoint& bp); + void Add(TBreakPoint bp); // Modify Breakpoint bool ToggleBreakPoint(u32 address); diff --git a/Source/Core/Core/PowerPC/Expression.cpp b/Source/Core/Core/PowerPC/Expression.cpp new file mode 100644 index 0000000000..d54ba84e71 --- /dev/null +++ b/Source/Core/Core/PowerPC/Expression.cpp @@ -0,0 +1,130 @@ +// Copyright 2020 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/PowerPC/Expression.h" + +#include +#include +#include +#include +#include + +#include + +#include "Core/PowerPC/PowerPC.h" + +void ExprDeleter::operator()(expr* expression) const +{ + expr_destroy(expression, nullptr); +} + +void ExprVarListDeleter::operator()(expr_var_list* vars) const +{ + // Free list elements + expr_destroy(nullptr, vars); + // Free list object + delete vars; +} + +Expression::Expression(std::string_view text, ExprPointer ex, ExprVarListPointer vars) + : m_text(text), m_expr(std::move(ex)), m_vars(std::move(vars)) +{ + for (auto* v = m_vars->head; v != nullptr; v = v->next) + { + const std::string_view name = v->name; + VarBinding bind; + + if (name.length() >= 2 && name.length() <= 3) + { + if (name[0] == 'r' || name[0] == 'f') + { + char* end = nullptr; + const int index = std::strtol(name.data() + 1, &end, 10); + if (index >= 0 && index <= 31 && end == name.data() + name.length()) + { + bind.type = name[0] == 'r' ? VarBindingType::GPR : VarBindingType::FPR; + bind.index = index; + } + } + else if (name == "lr") + { + bind.type = VarBindingType::SPR; + bind.index = SPR_LR; + } + else if (name == "ctr") + { + bind.type = VarBindingType::SPR; + bind.index = SPR_CTR; + } + else if (name == "pc") + { + bind.type = VarBindingType::PCtr; + } + } + + m_binds.emplace_back(bind); + } +} + +std::optional Expression::TryParse(std::string_view text) +{ + ExprVarListPointer vars{new expr_var_list{}}; + ExprPointer ex{expr_create(text.data(), text.length(), vars.get(), nullptr)}; + if (!ex) + return std::nullopt; + + return Expression{text, std::move(ex), std::move(vars)}; +} + +double Expression::Evaluate() const +{ + SynchronizeBindings(SynchronizeDirection::From); + + double result = expr_eval(m_expr.get()); + + SynchronizeBindings(SynchronizeDirection::To); + + return result; +} + +void Expression::SynchronizeBindings(SynchronizeDirection dir) const +{ + auto bind = m_binds.begin(); + for (auto* v = m_vars->head; v != nullptr; v = v->next, ++bind) + { + switch (bind->type) + { + case VarBindingType::Zero: + if (dir == SynchronizeDirection::From) + v->value = 0; + break; + case VarBindingType::GPR: + if (dir == SynchronizeDirection::From) + v->value = static_cast(GPR(bind->index)); + else + GPR(bind->index) = static_cast(static_cast(v->value)); + break; + case VarBindingType::FPR: + if (dir == SynchronizeDirection::From) + v->value = rPS(bind->index).PS0AsDouble(); + else + rPS(bind->index).SetPS0(v->value); + break; + case VarBindingType::SPR: + if (dir == SynchronizeDirection::From) + v->value = static_cast(rSPR(bind->index)); + else + rSPR(bind->index) = static_cast(static_cast(v->value)); + break; + case VarBindingType::PCtr: + if (dir == SynchronizeDirection::From) + v->value = static_cast(PC); + break; + } + } +} + +std::string Expression::GetText() const +{ + return m_text; +} diff --git a/Source/Core/Core/PowerPC/Expression.h b/Source/Core/Core/PowerPC/Expression.h new file mode 100644 index 0000000000..031b796ffe --- /dev/null +++ b/Source/Core/Core/PowerPC/Expression.h @@ -0,0 +1,73 @@ +// Copyright 2020 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +struct expr; +struct expr_var_list; + +struct ExprDeleter +{ + void operator()(expr* expression) const; +}; + +using ExprPointer = std::unique_ptr; + +struct ExprVarListDeleter +{ + void operator()(expr_var_list* vars) const; +}; + +using ExprVarListPointer = std::unique_ptr; + +class Expression +{ +public: + static std::optional TryParse(std::string_view text); + + double Evaluate() const; + + std::string GetText() const; + +private: + enum class SynchronizeDirection + { + From, + To, + }; + + enum class VarBindingType + { + Zero, + GPR, + FPR, + SPR, + PCtr, + }; + + struct VarBinding + { + VarBindingType type = VarBindingType::Zero; + int index = -1; + }; + + Expression(std::string_view text, ExprPointer ex, ExprVarListPointer vars); + + void SynchronizeBindings(SynchronizeDirection dir) const; + + std::string m_text; + ExprPointer m_expr; + ExprVarListPointer m_vars; + std::vector m_binds; +}; + +inline bool EvaluateCondition(const std::optional& condition) +{ + return !condition || condition->Evaluate() != 0.0; +} diff --git a/Source/Core/Core/PowerPC/PowerPC.cpp b/Source/Core/Core/PowerPC/PowerPC.cpp index aa6f7fbe11..d25deca939 100644 --- a/Source/Core/Core/PowerPC/PowerPC.cpp +++ b/Source/Core/Core/PowerPC/PowerPC.cpp @@ -610,16 +610,18 @@ void CheckExternalExceptions() void CheckBreakPoints() { - if (!PowerPC::breakpoints.IsBreakPointEnable(PC)) + const TBreakPoint* bp = PowerPC::breakpoints.GetBreakpoint(PC); + + if (bp == nullptr) return; - if (PowerPC::breakpoints.IsBreakPointBreakOnHit(PC)) + if (bp->break_on_hit) { CPU::Break(); if (GDBStub::IsActive()) GDBStub::TakeControl(); } - if (PowerPC::breakpoints.IsBreakPointLogOnHit(PC)) + if (bp->log_on_hit) { NOTICE_LOG_FMT(MEMMAP, "BP {:08x} {}({:08x} {:08x} {:08x} {:08x} {:08x} {:08x} {:08x} {:08x} {:08x} " diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 4c08f0a6d8..fb524e8716 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -398,6 +398,7 @@ + @@ -1016,6 +1017,7 @@ + diff --git a/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp b/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp index c8d98aabae..5a88814a24 100644 --- a/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp +++ b/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp @@ -15,6 +15,7 @@ #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/PowerPC/BreakPoints.h" +#include "Core/PowerPC/Expression.h" #include "Core/PowerPC/PPCSymbolDB.h" #include "Core/PowerPC/PowerPC.h" @@ -86,7 +87,7 @@ void BreakpointWidget::CreateWidgets() m_table = new QTableWidget; m_table->setTabKeyNavigation(false); m_table->setContentsMargins(0, 0, 0, 0); - m_table->setColumnCount(5); + m_table->setColumnCount(6); m_table->setSelectionMode(QAbstractItemView::SingleSelection); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -160,7 +161,7 @@ void BreakpointWidget::Update() m_table->clear(); m_table->setHorizontalHeaderLabels( - {tr("Active"), tr("Type"), tr("Function"), tr("Address"), tr("Flags")}); + {tr("Active"), tr("Type"), tr("Function"), tr("Address"), tr("Flags"), tr("Condition")}); int i = 0; m_table->setRowCount(i); @@ -203,6 +204,13 @@ void BreakpointWidget::Update() m_table->setItem(i, 4, create_item(flags)); + QString condition; + + if (bp.condition) + condition = QString::fromStdString(bp.condition->GetText()); + + m_table->setItem(i, 5, create_item(condition)); + i++; } @@ -387,12 +395,15 @@ void BreakpointWidget::OnContextMenu() void BreakpointWidget::AddBP(u32 addr) { - AddBP(addr, false, true, true); + AddBP(addr, false, true, true, {}); } -void BreakpointWidget::AddBP(u32 addr, bool temp, bool break_on_hit, bool log_on_hit) +void BreakpointWidget::AddBP(u32 addr, bool temp, bool break_on_hit, bool log_on_hit, + const QString& condition) { - PowerPC::breakpoints.Add(addr, temp, break_on_hit, log_on_hit); + PowerPC::breakpoints.Add( + addr, temp, break_on_hit, log_on_hit, + !condition.isEmpty() ? Expression::TryParse(condition.toUtf8().constData()) : std::nullopt); emit BreakpointsChanged(); Update(); diff --git a/Source/Core/DolphinQt/Debugger/BreakpointWidget.h b/Source/Core/DolphinQt/Debugger/BreakpointWidget.h index 782f20c1ff..21701a0654 100644 --- a/Source/Core/DolphinQt/Debugger/BreakpointWidget.h +++ b/Source/Core/DolphinQt/Debugger/BreakpointWidget.h @@ -21,7 +21,7 @@ public: ~BreakpointWidget(); void AddBP(u32 addr); - void AddBP(u32 addr, bool temp, bool break_on_hit, bool log_on_hit); + void AddBP(u32 addr, bool temp, bool break_on_hit, bool log_on_hit, const QString& condition); void AddAddressMBP(u32 addr, bool on_read = true, bool on_write = true, bool do_log = true, bool do_break = true); void AddRangedMBP(u32 from, u32 to, bool do_read = true, bool do_write = true, bool do_log = true, diff --git a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp index 3e8bfae69a..74123a10c1 100644 --- a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp +++ b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp @@ -14,6 +14,7 @@ #include #include +#include "Core/PowerPC/Expression.h" #include "DolphinQt/Debugger/BreakpointWidget.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" @@ -40,11 +41,14 @@ void NewBreakpointDialog::CreateWidgets() type_group->addButton(m_instruction_bp); m_instruction_box = new QGroupBox; m_instruction_address = new QLineEdit; + m_instruction_condition = new QLineEdit; - auto* instruction_layout = new QHBoxLayout; + auto* instruction_layout = new QGridLayout; m_instruction_box->setLayout(instruction_layout); - instruction_layout->addWidget(new QLabel(tr("Address:"))); - instruction_layout->addWidget(m_instruction_address); + instruction_layout->addWidget(new QLabel(tr("Address:")), 0, 0); + instruction_layout->addWidget(m_instruction_address, 0, 1); + instruction_layout->addWidget(new QLabel(tr("Condition:")), 1, 0); + instruction_layout->addWidget(m_instruction_condition, 1, 1); // Memory BP m_memory_bp = new QRadioButton(tr("Memory Breakpoint")); @@ -174,7 +178,15 @@ void NewBreakpointDialog::accept() return; } - m_parent->AddBP(address, false, do_break, do_log); + const QString condition = m_instruction_condition->text().trimmed(); + + if (!condition.isEmpty() && !Expression::TryParse(condition.toUtf8().constData())) + { + invalid_input(tr("Condition")); + return; + } + + m_parent->AddBP(address, false, do_break, do_log, condition); } else { diff --git a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.h b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.h index 8251e60f50..c7fd4c3565 100644 --- a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.h +++ b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.h @@ -34,6 +34,7 @@ private: QRadioButton* m_instruction_bp; QGroupBox* m_instruction_box; QLineEdit* m_instruction_address; + QLineEdit* m_instruction_condition; // Memory BPs QRadioButton* m_memory_bp; diff --git a/Source/VSProps/Base.Dolphin.props b/Source/VSProps/Base.Dolphin.props index 43af6653a1..b1ef86f6dc 100644 --- a/Source/VSProps/Base.Dolphin.props +++ b/Source/VSProps/Base.Dolphin.props @@ -12,6 +12,7 @@ $(ExternalsDir)FFmpeg-bin\$(Platform)\include;%(AdditionalIncludeDirectories) $(ExternalsDir)OpenAL\include;%(AdditionalIncludeDirectories) + $(ExternalsDir)expr\include;%(AdditionalIncludeDirectories) $(ExternalsDir)rangeset\include;%(AdditionalIncludeDirectories) $(ExternalsDir)Vulkan\include;%(AdditionalIncludeDirectories) $(ExternalsDir)WIL\include;%(AdditionalIncludeDirectories) From 11851a01e62fa698a7aad392c4719a9be1e3881f Mon Sep 17 00:00:00 2001 From: smurf3tte <75271109+smurf3tte@users.noreply.github.com> Date: Thu, 17 Dec 2020 17:01:43 -0800 Subject: [PATCH 2/7] Expand expr's support for numeric literals - Support hex prefix 0x or OX - Support scientific notation Also, reconcile the bitwise complement operator with C (use ~ instead of ^). --- Externals/expr/include/expr.h | 66 ++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/Externals/expr/include/expr.h b/Externals/expr/include/expr.h index 59ba4dbaad..c9475e4521 100644 --- a/Externals/expr/include/expr.h +++ b/Externals/expr/include/expr.h @@ -160,10 +160,10 @@ static int expr_prec(enum expr_type a, enum expr_type b) { return (left && prec[a] >= prec[b]) || (prec[a] > prec[b]); } -#define isfirstvarchr(c) \ - (((unsigned char)c >= '@' && c != '^' && c != '|') || c == '$') -#define isvarchr(c) \ - (((unsigned char)c >= '@' && c != '^' && c != '|') || c == '$' || \ +#define isfirstvarchr(c) \ + (((unsigned char)c >= '@' && c != '^' && c != '|' && c != '~') || c == '$') +#define isvarchr(c) \ + (((unsigned char)c >= '@' && c != '^' && c != '|' && c != '~') || c == '$' || \ c == '#' || (c >= '0' && c <= '9')) static struct { @@ -172,7 +172,7 @@ static struct { } OPS[] = { {"-u", OP_UNARY_MINUS}, {"!u", OP_UNARY_LOGICAL_NOT}, - {"^u", OP_UNARY_BITWISE_NOT}, + {"~u", OP_UNARY_BITWISE_NOT}, {"**", OP_POWER}, {"*", OP_MULTIPLY}, {"/", OP_DIVIDE}, @@ -199,7 +199,7 @@ static struct { them at the end */ {"-", OP_UNARY_MINUS}, {"!", OP_UNARY_LOGICAL_NOT}, - {"^", OP_UNARY_BITWISE_NOT}, + {"~", OP_UNARY_BITWISE_NOT}, }; static enum expr_type expr_op(const char *s, size_t len, int unary) { @@ -212,30 +212,24 @@ static enum expr_type expr_op(const char *s, size_t len, int unary) { return OP_UNKNOWN; } -static double expr_parse_number(const char *s, size_t len) { +static double expr_parse_number(const char* s, size_t len) { double num = 0; - unsigned int frac = 0; - unsigned int digits = 0; - for (unsigned int i = 0; i < len; i++) { - if (s[i] == '.' && frac == 0) { - frac++; - continue; - } - if (isdigit(s[i])) { - digits++; - if (frac > 0) { - frac++; - } - num = num * 10 + (s[i] - '0'); - } else { + char buf[32]; + char* sz = buf; + char* end = NULL; + if (len >= sizeof(buf)) { + sz = (char*)calloc(1, len + 1); + if (sz == NULL) { return NAN; } } - while (frac > 1) { - num = num / 10; - frac--; + strncpy(sz, s, len); + sz[len] = '\0'; + num = strtod(sz, &end); + if (sz != buf) { + free(sz); } - return (digits > 0 ? num : NAN); + return (end == sz + len ? num : NAN); } /* @@ -446,9 +440,23 @@ static int expr_next_token(const char *s, size_t len, int *flags) { return -1; // unexpected number } *flags = EXPR_TOP | EXPR_TCLOSE; - while ((c == '.' || isdigit(c)) && i < len) { + if (c == '0') { i++; - c = s[i]; + if (i < len && (s[i] == 'x' || s[i] == 'X')) { + i++; + for (; i < len && isxdigit(s[i]); i++) + ; + return i; + } + } + for (; i < len && (s[i] == '.' || isdigit(s[i])); i++) + ; + if (i < len && (s[i] == 'e' || s[i] == 'E')) { + i++; + if (i < len && (s[i] == '+' || s[i] == '-')) + i++; + for (; i < len && isdigit(s[i]); i++) + ; } return i; } else if (isfirstvarchr(c)) { @@ -626,8 +634,8 @@ static struct expr *expr_create(const char *s, size_t len, case '-': tok = "-u"; break; - case '^': - tok = "^u"; + case '~': + tok = "~u"; break; case '!': tok = "!u"; From 341792016746eecd2fef255a4d4c62a8332f9b50 Mon Sep 17 00:00:00 2001 From: smurf3tte <75271109+smurf3tte@users.noreply.github.com> Date: Sat, 19 Dec 2020 12:49:19 -0800 Subject: [PATCH 3/7] Grow internal expr int representation to 64 bits This is necessary to retain precision above 32 bits, but more importantly to prevent an expression like (0x80000000 | 1) from flipping the sign of the result. --- Externals/expr/include/expr.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Externals/expr/include/expr.h b/Externals/expr/include/expr.h index c9475e4521..f531f378dd 100644 --- a/Externals/expr/include/expr.h +++ b/Externals/expr/include/expr.h @@ -14,6 +14,7 @@ extern "C" { #include /* for isspace */ #include #include /* for pow */ +#include #include #include #include @@ -288,13 +289,13 @@ static struct expr_var *expr_get_var(struct expr_var_list *vars, const char *s, return v; } -static int to_int(double x) { +static int64_t to_int(double x) { if (isnan(x)) { return 0; } else if (isinf(x) != 0) { - return INT_MAX * isinf(x); + return INT64_MAX * isinf(x); } else { - return (int)x; + return (int64_t)x; } } From 1224575b0be603562af65009287c4b501ae0a97f Mon Sep 17 00:00:00 2001 From: smurf3tte <75271109+smurf3tte@users.noreply.github.com> Date: Sat, 19 Dec 2020 13:33:47 -0800 Subject: [PATCH 4/7] Add cast expression functions u8(), s8(), u16(), etc. This allows for easy reinterpretation of GPRs in an expression. Co-authored-by: TryTwo --- Source/Core/Core/PowerPC/Expression.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/PowerPC/Expression.cpp b/Source/Core/Core/PowerPC/Expression.cpp index d54ba84e71..cde531e94b 100644 --- a/Source/Core/Core/PowerPC/Expression.cpp +++ b/Source/Core/Core/PowerPC/Expression.cpp @@ -3,6 +3,7 @@ #include "Core/PowerPC/Expression.h" +#include #include #include #include @@ -11,8 +12,28 @@ #include +#include "Common/BitUtils.h" +#include "Common/CommonTypes.h" #include "Core/PowerPC/PowerPC.h" +template +static double CastFunc(expr_func* f, vec_expr_t* args, void* c) +{ + if (vec_len(args) != 1) + return 0; + return Common::BitCast(static_cast(expr_eval(&vec_nth(args, 0)))); +} + +static std::array g_expr_funcs{{ + {"u8", CastFunc}, + {"s8", CastFunc}, + {"u16", CastFunc}, + {"s16", CastFunc}, + {"u32", CastFunc}, + {"s32", CastFunc}, + {}, +}}; + void ExprDeleter::operator()(expr* expression) const { expr_destroy(expression, nullptr); @@ -69,7 +90,7 @@ Expression::Expression(std::string_view text, ExprPointer ex, ExprVarListPointer std::optional Expression::TryParse(std::string_view text) { ExprVarListPointer vars{new expr_var_list{}}; - ExprPointer ex{expr_create(text.data(), text.length(), vars.get(), nullptr)}; + ExprPointer ex{expr_create(text.data(), text.length(), vars.get(), g_expr_funcs.data())}; if (!ex) return std::nullopt; From 85cb4d1bc57044a00a61ade2218c94435ee0174c Mon Sep 17 00:00:00 2001 From: smurf3tte <75271109+smurf3tte@users.noreply.github.com> Date: Thu, 17 Dec 2020 21:38:29 -0800 Subject: [PATCH 5/7] Support memory reads/writes from breakpoint expressions Co-authored-by: TryTwo --- Source/Core/Core/PowerPC/Expression.cpp | 92 ++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/PowerPC/Expression.cpp b/Source/Core/Core/PowerPC/Expression.cpp index cde531e94b..e4f84664d1 100644 --- a/Source/Core/Core/PowerPC/Expression.cpp +++ b/Source/Core/Core/PowerPC/Expression.cpp @@ -14,8 +14,83 @@ #include "Common/BitUtils.h" #include "Common/CommonTypes.h" +#include "Core/PowerPC/MMU.h" #include "Core/PowerPC/PowerPC.h" +template +static T HostRead(u32 address); + +template +static void HostWrite(T var, u32 address); + +template <> +u8 HostRead(u32 address) +{ + return PowerPC::HostRead_U8(address); +} + +template <> +u16 HostRead(u32 address) +{ + return PowerPC::HostRead_U16(address); +} + +template <> +u32 HostRead(u32 address) +{ + return PowerPC::HostRead_U32(address); +} + +template <> +u64 HostRead(u32 address) +{ + return PowerPC::HostRead_U64(address); +} + +template <> +void HostWrite(u8 var, u32 address) +{ + PowerPC::HostWrite_U8(var, address); +} + +template <> +void HostWrite(u16 var, u32 address) +{ + PowerPC::HostWrite_U16(var, address); +} + +template <> +void HostWrite(u32 var, u32 address) +{ + PowerPC::HostWrite_U32(var, address); +} + +template <> +void HostWrite(u64 var, u32 address) +{ + PowerPC::HostWrite_U64(var, address); +} + +template +static double HostReadFunc(expr_func* f, vec_expr_t* args, void* c) +{ + if (vec_len(args) != 1) + return 0; + const u32 address = static_cast(expr_eval(&vec_nth(args, 0))); + return Common::BitCast(HostRead(address)); +} + +template +static double HostWriteFunc(expr_func* f, vec_expr_t* args, void* c) +{ + if (vec_len(args) != 2) + return 0; + const T var = static_cast(expr_eval(&vec_nth(args, 0))); + const u32 address = static_cast(expr_eval(&vec_nth(args, 1))); + HostWrite(Common::BitCast(var), address); + return var; +} + template static double CastFunc(expr_func* f, vec_expr_t* args, void* c) { @@ -24,7 +99,22 @@ static double CastFunc(expr_func* f, vec_expr_t* args, void* c) return Common::BitCast(static_cast(expr_eval(&vec_nth(args, 0)))); } -static std::array g_expr_funcs{{ +static std::array g_expr_funcs{{ + // For internal storage and comparisons, everything is auto-converted to Double. + // If u64 ints are added, this could produce incorrect results. + {"read_u8", HostReadFunc}, + {"read_s8", HostReadFunc}, + {"read_u16", HostReadFunc}, + {"read_s16", HostReadFunc}, + {"read_u32", HostReadFunc}, + {"read_s32", HostReadFunc}, + {"read_f32", HostReadFunc}, + {"read_f64", HostReadFunc}, + {"write_u8", HostWriteFunc}, + {"write_u16", HostWriteFunc}, + {"write_u32", HostWriteFunc}, + {"write_f32", HostWriteFunc}, + {"write_f64", HostWriteFunc}, {"u8", CastFunc}, {"s8", CastFunc}, {"u16", CastFunc}, From 1ed6028af415075e7cc2b251415dd97ee477002b Mon Sep 17 00:00:00 2001 From: TryTwo Date: Sat, 8 Oct 2022 13:23:24 -0700 Subject: [PATCH 6/7] NewBreakpointDialog: Add a help message for conditionals. --- .../Debugger/NewBreakpointDialog.cpp | 49 ++++++++++++++++++- .../DolphinQt/Debugger/NewBreakpointDialog.h | 2 + 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp index 74123a10c1..b645377457 100644 --- a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp +++ b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -36,12 +37,18 @@ void NewBreakpointDialog::CreateWidgets() auto* type_group = new QButtonGroup(this); // Instruction BP + auto* top_layout = new QHBoxLayout; m_instruction_bp = new QRadioButton(tr("Instruction Breakpoint")); m_instruction_bp->setChecked(true); type_group->addButton(m_instruction_bp); m_instruction_box = new QGroupBox; m_instruction_address = new QLineEdit; m_instruction_condition = new QLineEdit; + m_cond_help_btn = new QPushButton(tr("Help")); + + top_layout->addWidget(m_instruction_bp); + top_layout->addStretch(); + top_layout->addWidget(m_cond_help_btn); auto* instruction_layout = new QGridLayout; m_instruction_box->setLayout(instruction_layout); @@ -106,7 +113,7 @@ void NewBreakpointDialog::CreateWidgets() auto* layout = new QVBoxLayout; - layout->addWidget(m_instruction_bp); + layout->addLayout(top_layout); layout->addWidget(m_instruction_box); layout->addWidget(m_memory_bp); layout->addWidget(m_memory_box); @@ -123,6 +130,8 @@ void NewBreakpointDialog::ConnectWidgets() connect(m_buttons, &QDialogButtonBox::accepted, this, &NewBreakpointDialog::accept); connect(m_buttons, &QDialogButtonBox::rejected, this, &NewBreakpointDialog::reject); + connect(m_cond_help_btn, &QPushButton::clicked, this, &NewBreakpointDialog::ShowConditionHelp); + connect(m_instruction_bp, &QRadioButton::toggled, this, &NewBreakpointDialog::OnBPTypeChanged); connect(m_memory_bp, &QRadioButton::toggled, this, &NewBreakpointDialog::OnBPTypeChanged); @@ -217,3 +226,41 @@ void NewBreakpointDialog::accept() QDialog::accept(); } + +void NewBreakpointDialog::ShowConditionHelp() +{ + const auto message = QStringLiteral( + "Set a code breakpoint for when an instruction is executed. Use with the code widget.\n" + "\n" + "Conditions:\n" + "Sets an expression that is evaluated when a breakpoint is hit. If the expression is false " + "or 0, the breakpoint is ignored until hit again. Statements should be separated by a comma. " + "Only the last statement will be used to determine what to do.\n" + "\n" + "Registers that can be referenced:\n" + "GPRs : r0..r31\n" + "FPRs : f0..f31\n LR, CTR, PC\n" + "\n" + "Functions:\n" + "Set a register: r1 = 8\n" + "Casts: s8(0xff). Available: s8, u8, s16, u16, s32, u32\n" + "Read Memory: read_u32(0x80000000). Available: u8, s8, u16, s16, u32, s32, f32, f64\n" + "Write Memory: write_u32(r3, 0x80000000). Available: u8, u16, u32, f32, f64\n" + "*currently writing will always be triggered\n" + "\n" + "Operations:\n" + "Unary: -u, !u, ~u\n" + "Math: * / + -, power: **, remainder: %, shift: <<, >>\n" + "Compare: <, <=, >, >=, ==, !=, &&, ||\n" + "Bitwise: &, |, ^\n" + "\n" + "Examples:\n" + "r4 == 1\n" + "f0 == 1.0 && f2 < 10.0\n" + "r26 <= r0 && ((r5 + 3) & -4) * ((r6 + 3) & -4)* 4 > r0\n" + "p = r3 + 0x8, p == 0x8003510 && read_u32(p) != 0\n" + "Write and break: r4 = 8, 1\n" + "Write and continue: f3 = f1 + f2, 0\n" + "The condition must always be last\n"); + ModalMessageBox::information(this, tr("Conditional help"), message); +} diff --git a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.h b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.h index c7fd4c3565..89c51629e0 100644 --- a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.h +++ b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.h @@ -29,12 +29,14 @@ private: void OnBPTypeChanged(); void OnAddressTypeChanged(); + void ShowConditionHelp(); // Instruction BPs QRadioButton* m_instruction_bp; QGroupBox* m_instruction_box; QLineEdit* m_instruction_address; QLineEdit* m_instruction_condition; + QPushButton* m_cond_help_btn; // Memory BPs QRadioButton* m_memory_bp; From e2df81b0c157c0da2cdac99feb42e3004dc2e499 Mon Sep 17 00:00:00 2001 From: TryTwo Date: Sat, 8 Oct 2022 14:54:10 -0700 Subject: [PATCH 7/7] Add logging and error reporting to Breakpoint Conditionals. --- Source/Core/Core/PowerPC/Expression.cpp | 28 +++++++++++++++++++ Source/Core/Core/PowerPC/Expression.h | 1 + .../Debugger/NewBreakpointDialog.cpp | 7 ++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/PowerPC/Expression.cpp b/Source/Core/Core/PowerPC/Expression.cpp index e4f84664d1..b25149c41f 100644 --- a/Source/Core/Core/PowerPC/Expression.cpp +++ b/Source/Core/Core/PowerPC/Expression.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -14,6 +15,8 @@ #include "Common/BitUtils.h" #include "Common/CommonTypes.h" +#include "Common/Logging/Log.h" +#include "Core/Core.h" #include "Core/PowerPC/MMU.h" #include "Core/PowerPC/PowerPC.h" @@ -195,6 +198,8 @@ double Expression::Evaluate() const SynchronizeBindings(SynchronizeDirection::To); + Reporting(result); + return result; } @@ -235,6 +240,29 @@ void Expression::SynchronizeBindings(SynchronizeDirection dir) const } } +void Expression::Reporting(const double result) const +{ + bool is_nan = std::isnan(result); + std::string message; + + for (auto* v = m_vars->head; v != nullptr; v = v->next) + { + if (std::isnan(v->value)) + is_nan = true; + + fmt::format_to(std::back_inserter(message), " {}={}", v->name, v->value); + } + + if (is_nan) + { + message.append("\nBreakpoint condition encountered a NaN"); + Core::DisplayMessage("Breakpoint condition has encountered a NaN.", 2000); + } + + if (result != 0.0 || is_nan) + NOTICE_LOG_FMT(MEMMAP, "Breakpoint condition returned: {}. Vars:{}", result, message); +} + std::string Expression::GetText() const { return m_text; diff --git a/Source/Core/Core/PowerPC/Expression.h b/Source/Core/Core/PowerPC/Expression.h index 031b796ffe..7fb5370328 100644 --- a/Source/Core/Core/PowerPC/Expression.h +++ b/Source/Core/Core/PowerPC/Expression.h @@ -60,6 +60,7 @@ private: Expression(std::string_view text, ExprPointer ex, ExprVarListPointer vars); void SynchronizeBindings(SynchronizeDirection dir) const; + void Reporting(const double result) const; std::string m_text; ExprPointer m_expr; diff --git a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp index b645377457..d28c2dc61e 100644 --- a/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp +++ b/Source/Core/DolphinQt/Debugger/NewBreakpointDialog.cpp @@ -261,6 +261,11 @@ void NewBreakpointDialog::ShowConditionHelp() "p = r3 + 0x8, p == 0x8003510 && read_u32(p) != 0\n" "Write and break: r4 = 8, 1\n" "Write and continue: f3 = f1 + f2, 0\n" - "The condition must always be last\n"); + "The condition must always be last\n\n" + "All variables will be printed in the Memory Interface log, if there's a hit or a NaN " + "result. To check for issues, assign a variable to your equation, so it can be printed.\n\n" + "Note: All values are internally converted to Doubles for calculations. It's possible for " + "them to go out of range or to become NaN. A warning will be given if NaN is returned, and " + "the var that became NaN will be logged."); ModalMessageBox::information(this, tr("Conditional help"), message); }