// New bounded expression parser replacing historical mathemu; see docs/LINEAGE.md. #include "script.h" #include #include #include #include #include #include #include namespace vanilla { [[noreturn]] void fail(const Source& s, const std::string& message) { throw Error(s, message); } [[noreturn]] void failLimit(const Source& s, const std::string& message) { throw Error(s, message, true); } static bool digit(char c) { return c >= '0' && c <= '9'; } bool numeric(const std::string& s, double& result) { std::size_t p = 0; if (p < s.size() && (s[p] == '+' || s[p] == '-')) ++p; bool digits = false; while (p < s.size() && digit(s[p])) { ++p; digits = true; } if (p < s.size() && s[p] == '.') { ++p; while (p < s.size() && digit(s[p])) { ++p; digits = true; } } if (!digits) return false; if (p < s.size() && (s[p] == 'e' || s[p] == 'E')) { ++p; if (p < s.size() && (s[p] == '+' || s[p] == '-')) ++p; auto begin = p; while (p < s.size() && digit(s[p])) ++p; if (p == begin) return false; } if (p != s.size()) return false; errno = 0; char* end = nullptr; result = std::strtod(s.c_str(), &end); return end == s.c_str()+s.size() && errno != ERANGE && std::isfinite(result); } double number(const std::string& s, const Source& source) { double n; if (!numeric(s, n)) fail(source, "expected finite decimal number: " + s.substr(0,80)); return n; } std::string format(double n, const Source& s) { if (!std::isfinite(n)) fail(s, "numeric result must be finite"); if (n == 0) return "0"; std::ostringstream out; out << std::setprecision(17) << n; return out.str(); } long long integer(const std::string& s, long long low, long long high, const Source& source) { // All callers use bounds inside the exactly representable double integer range. double n; if (!numeric(s,n) || std::trunc(n) != n || n < static_cast(low) || n > static_cast(high)) fail(source,"expected integer in bounds"); return static_cast(n); } class Expression { const std::string& text; const Source& source; const std::function& resolve; MathCache& cache; void emit(MathOp op, double n = 0, std::string ref = {}) { cache.code.push_back({op,n,std::move(ref)}); } std::size_t pos = 0; void space() { while(pos < text.size() && std::isspace(static_cast(text[pos]))) ++pos; } char peek() { space(); return pos < text.size() ? text[pos] : '\0'; } bool take(char c) { if(peek() == c) { ++pos; return true; } return false; } [[noreturn]] void bad() { fail(source,"invalid expression near column " + std::to_string(pos+1)); } double checked(double n) { if(!std::isfinite(n)) fail(source,"numeric result must be finite"); return n; } double parse(int minimum, unsigned depth) { if (depth > 128) failLimit(source,"expression nesting limit (128)"); char c = peek(); double left = 0; if (c == '+' || c == '-') { ++pos; left = parse(30,depth+1); if(c == '-') { left = -left; emit(MathOp::neg); } } else if(take('(')) { left = parse(0,depth+1); if(!take(')')) bad(); } else if(c == '$' || c == '%') { auto begin = pos++; while(pos < text.size() && (std::isalnum(static_cast(text[pos])) || text[pos] == '_')) ++pos; if(pos == begin+1) bad(); auto ref = text.substr(begin,pos-begin); left = resolve(ref); emit(MathOp::reference,0,std::move(ref)); } else if(std::isalpha(static_cast(c))) { auto begin = pos++; while(pos < text.size() && std::isalpha(static_cast(text[pos]))) ++pos; auto name = text.substr(begin,pos-begin); if(!take('(')) bad(); double a = parse(0,depth+1), b = 0; bool binary = take(','); if(binary) b = parse(0,depth+1); if(!take(')')) bad(); if(binary && name == "min") left = std::min(a,b); else if(binary && name == "max") left = std::max(a,b); else if(binary && name == "pow") left = std::pow(a,b); else if(!binary && name == "abs") left = std::abs(a); else if(!binary && name == "sqrt") left = std::sqrt(a); else if(!binary && name == "floor") left = std::floor(a); else if(!binary && name == "ceil") left = std::ceil(a); else if(!binary && name == "round") left = std::round(a); else fail(source,"unknown math function or argument count: " + name); checked(left); emit(binary ? (name == "min" ? MathOp::minimum : name == "max" ? MathOp::maximum : MathOp::power) : name == "abs" ? MathOp::absolute : name == "sqrt" ? MathOp::root : name == "floor" ? MathOp::floor : name == "ceil" ? MathOp::ceil : MathOp::round); } else if(digit(c) || c == '.') { auto begin = pos; while(pos < text.size() && digit(text[pos])) ++pos; if(pos < text.size() && text[pos] == '.') { ++pos; while(pos < text.size() && digit(text[pos])) ++pos; } if(pos < text.size() && (text[pos] == 'e' || text[pos] == 'E')) { ++pos; if(pos < text.size() && (text[pos] == '+' || text[pos] == '-')) ++pos; while(pos < text.size() && digit(text[pos])) ++pos; } left = number(text.substr(begin,pos-begin),source); emit(MathOp::constant,left); } else bad(); while(true) { c = peek(); int precedence = c == '+' || c == '-' ? 10 : c == '*' || c == '/' || c == '%' ? 20 : c == '^' ? 40 : -1; if(precedence < minimum) break; ++pos; double right = parse(precedence + (c == '^' ? 0 : 1),depth+1); if(c == '+') left += right; else if(c == '-') left -= right; else if(c == '*') left *= right; else if(c == '^') left = std::pow(left,right); else { if(right == 0) fail(source,"division or modulo by zero"); if(c == '/') left /= right; else left = std::fmod(left,right); } checked(left); emit(c == '+' ? MathOp::add : c == '-' ? MathOp::sub : c == '*' ? MathOp::mul : c == '^' ? MathOp::power : c == '/' ? MathOp::div : MathOp::mod); } return left; } public: Expression(const std::string& t, const Source& s, const std::function& r, MathCache& m): text(t),source(s),resolve(r),cache(m) {} double run() { auto n = parse(0,0); if(peek() != '\0') bad(); return n; } }; double expression(const std::string& text, const Source& s, const std::function& resolve, MathCache& cache) { if(!cache.ready) { if(text.size() > 16384) failLimit(s,"expression size limit (16384)"); // Record while evaluating, rather than parsing ahead of runtime failures. // This preserves the original first error (e.g. 1/0 followed by bad syntax). cache.code.clear(); auto result = Expression(text,s,resolve,cache).run(); // Each emitted instruction consumes source characters. Size is bounded // by the 16384-byte expression limit; allocate once, never per iteration. cache.stack.resize(cache.code.size()); cache.ready = true; return result; } std::size_t top = 0; for(const auto& instruction:cache.code) { auto op = instruction.op; if(op == MathOp::constant || op == MathOp::reference) { cache.stack[top++] = op == MathOp::constant ? instruction.constant : resolve(instruction.reference); continue; } double right = 0; if(op >= MathOp::add && op <= MathOp::maximum) right = cache.stack[--top]; double& left = cache.stack[top-1]; switch(op) { case MathOp::neg: left = -left; break; case MathOp::add: left += right; break; case MathOp::sub: left -= right; break; case MathOp::mul: left *= right; break; case MathOp::div: case MathOp::mod: if(right == 0) fail(s,"division or modulo by zero"); left = op == MathOp::div ? left/right : std::fmod(left,right); break; case MathOp::power: left = std::pow(left,right); break; case MathOp::minimum: left = std::min(left,right); break; case MathOp::maximum: left = std::max(left,right); break; case MathOp::absolute: left = std::abs(left); break; case MathOp::root: left = std::sqrt(left); break; case MathOp::floor: left = std::floor(left); break; case MathOp::ceil: left = std::ceil(left); break; case MathOp::round: left = std::round(left); break; default: break; // constants/references handled above } if(!std::isfinite(left)) fail(s,"numeric result must be finite"); } return cache.stack[0]; } } // namespace vanilla