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)