From a15ca3add779596ec2bd5bd2207051739fbf5ec7 Mon Sep 17 00:00:00 2001 From: Francesco Cozzuto Date: Sun, 7 Apr 2024 17:55:16 +0200 Subject: [PATCH] first commit --- .gitignore | 2 + README.txt | 4 + UNLICENSE | 24 +++ eval.cpp | 129 +++++++++++++++ eval.hpp | 3 + expr.hpp | 138 ++++++++++++++++ main.cpp | 52 ++++++ makefile | 3 + parse.cpp | 442 ++++++++++++++++++++++++++++++++++++++++++++++++++ parse.hpp | 4 + stringify.cpp | 101 ++++++++++++ stringify.hpp | 3 + 12 files changed, 905 insertions(+) create mode 100644 .gitignore create mode 100644 README.txt create mode 100644 UNLICENSE create mode 100644 eval.cpp create mode 100644 eval.hpp create mode 100644 expr.hpp create mode 100644 main.cpp create mode 100644 makefile create mode 100644 parse.cpp create mode 100644 parse.hpp create mode 100644 stringify.cpp create mode 100644 stringify.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3a7f03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +main +main.exe \ No newline at end of file diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..53d1706 --- /dev/null +++ b/README.txt @@ -0,0 +1,4 @@ +This repository implements a simple arithmetic expression parser and evaluator using the visitor pattern. + +The main inspiration was this article: + https://eli.thegreenplace.net/2016/the-expression-problem-and-its-solutions/ diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..00d2e13 --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to \ No newline at end of file diff --git a/eval.cpp b/eval.cpp new file mode 100644 index 0000000..13e87f7 --- /dev/null +++ b/eval.cpp @@ -0,0 +1,129 @@ +#include "expr.hpp" + +struct EvalVisitor : Visitor { + + static const uint32_t max_depth = 32; + + bool fail; + uint32_t depth; + double stack[max_depth]; + + EvalVisitor() + { + depth = 0; + fail = false; + } + + void visit_int(IntValue *node) + { + if (fail) return; + + if (depth == max_depth) { + fail = true; + return; + } + + stack[depth++] = node->val; + } + + void visit_float(FloatValue *node) + { + if (fail) return; + + if (depth == max_depth) { + fail = true; + return; + } + + stack[depth++] = node->val; + } + + void visit_neg(NegOperation *node) + { + node->op->visit(*this); + + if (fail) return; + + if (depth < 1) { + fail = true; + return; + } + + stack[depth-1] = -stack[depth-1]; + } + + void visit_add(AddOperation *node) + { + node->lhs->visit(*this); + node->rhs->visit(*this); + + if (fail) return; + + if (depth < 2) { + fail = true; + return; + } + + double rhs = stack[--depth]; + double lhs = stack[--depth]; + stack[depth++] = lhs + rhs; + } + + void visit_sub(SubOperation *node) + { + node->lhs->visit(*this); + node->rhs->visit(*this); + + if (fail) return; + + if (depth < 2) { + fail = true; + return; + } + + double rhs = stack[--depth]; + double lhs = stack[--depth]; + stack[depth++] = lhs - rhs; + } + + void visit_mul(MulOperation *node) + { + node->lhs->visit(*this); + node->rhs->visit(*this); + + if (fail) return; + + if (depth < 2) { + fail = true; + return; + } + + double rhs = stack[--depth]; + double lhs = stack[--depth]; + stack[depth++] = lhs * rhs; + } + + void visit_div(DivOperation *node) + { + node->lhs->visit(*this); + node->rhs->visit(*this); + + if (fail) return; + + if (depth < 2) { + fail = true; + return; + } + + double rhs = stack[--depth]; + double lhs = stack[--depth]; + stack[depth++] = lhs / rhs; + } +}; + +double eval(Expr *e) +{ + EvalVisitor visitor; + e->visit(visitor); + return visitor.stack[visitor.depth-1]; +} diff --git a/eval.hpp b/eval.hpp new file mode 100644 index 0000000..42b6ec5 --- /dev/null +++ b/eval.hpp @@ -0,0 +1,3 @@ +#include "expr.hpp" + +double eval(Expr *e); \ No newline at end of file diff --git a/expr.hpp b/expr.hpp new file mode 100644 index 0000000..2e6815c --- /dev/null +++ b/expr.hpp @@ -0,0 +1,138 @@ +#pragma once + +#include + +struct IntValue; +struct FloatValue; +struct NegOperation; +struct AddOperation; +struct SubOperation; +struct MulOperation; +struct DivOperation; + +struct Visitor { + virtual void visit_int(IntValue *node) = 0; + virtual void visit_float(FloatValue *node) = 0; + virtual void visit_neg(NegOperation *node) = 0; + virtual void visit_add(AddOperation *node) = 0; + virtual void visit_sub(SubOperation *node) = 0; + virtual void visit_mul(MulOperation *node) = 0; + virtual void visit_div(DivOperation *node) = 0; +}; + +struct Expr { + virtual void visit(Visitor &visitor) = 0; +}; + +struct IntValue : Expr { + + int64_t val; + + IntValue(int64_t val_) + { + val = val_; + } + + virtual void visit(Visitor &visitor) + { + visitor.visit_int(this); + } +}; + +struct FloatValue : Expr { + + double val; + + FloatValue(double val_) + { + val = val_; + } + + virtual void visit(Visitor &visitor) + { + visitor.visit_float(this); + } +}; + +struct NegOperation : Expr { + + Expr *op; + + NegOperation(Expr *op_) + { + op = op_; + } + + virtual void visit(Visitor &visitor) + { + visitor.visit_neg(this); + } +}; + +struct AddOperation : Expr { + + Expr *lhs; + Expr *rhs; + + AddOperation(Expr *lhs_, Expr *rhs_) + { + lhs = lhs_; + rhs = rhs_; + } + + virtual void visit(Visitor &visitor) + { + visitor.visit_add(this); + } +}; + +struct SubOperation : Expr { + + Expr *lhs; + Expr *rhs; + + SubOperation(Expr *lhs_, Expr *rhs_) + { + lhs = lhs_; + rhs = rhs_; + } + + virtual void visit(Visitor &visitor) + { + visitor.visit_sub(this); + } +}; + +struct MulOperation : Expr { + + Expr *lhs; + Expr *rhs; + + MulOperation(Expr *lhs_, Expr *rhs_) + { + lhs = lhs_; + rhs = rhs_; + } + + virtual void visit(Visitor &visitor) + { + visitor.visit_mul(this); + } +}; + +struct DivOperation : Expr { + + Expr *lhs; + Expr *rhs; + + DivOperation(Expr *lhs_, Expr *rhs_) + { + lhs = lhs_; + rhs = rhs_; + } + + virtual void visit(Visitor &visitor) + { + visitor.visit_div(this); + } +}; diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..e2ab27d --- /dev/null +++ b/main.cpp @@ -0,0 +1,52 @@ +#include +#include +#include "expr.hpp" +#include "eval.hpp" +#include "parse.hpp" +#include "stringify.hpp" + +uint32_t read_line(FILE *stream, char *dst, uint32_t max) +{ + uint32_t num = 0; + for (char c; (c = getc(stream)) != '\n'; ) + if (num < max) dst[num++] = c; + + if (max > 0) { + if (num == max) + num--; + dst[num] = '\0'; + } + return num; +} + +int main() +{ + for (;;) { + + uint32_t len; + char text[1<<9]; + char pool[1<<12]; + + /* + * Read an expression from stdin + */ + fprintf(stdout, "> "); + len = read_line(stdin, text, sizeof(text)); + + /* + * Parse it + */ + Expr *e = parse(text, len, pool, sizeof(pool)); + if (e == nullptr) { + fprintf(stderr, "Error\n"); + continue; + } + + /* + * Print the expression and its result + */ + stringify(e, text, sizeof(text)); + fprintf(stdout, "%s = %g\n", text, eval(e)); + } + return 0; +} \ No newline at end of file diff --git a/makefile b/makefile new file mode 100644 index 0000000..6c5c82b --- /dev/null +++ b/makefile @@ -0,0 +1,3 @@ + +all: + g++ main.cpp parse.cpp eval.cpp stringify.cpp -o main -Wall -Wextra -ggdb \ No newline at end of file diff --git a/parse.cpp b/parse.cpp new file mode 100644 index 0000000..df12502 --- /dev/null +++ b/parse.cpp @@ -0,0 +1,442 @@ +#include +#include +#include "parse.hpp" + +struct Arena { + char *base; + uint32_t size; + uint32_t used; + + Arena() + { + base = nullptr; + size = 0; + used = 0; + } + + void init(void *base_, uint32_t size_) + { + base = (char*) base_; + size = size_; + used = 0; + } + + static bool is_pow2(uint32_t n) + { + return (n & (n-1)) == 0; + } + + void *alloc(uint32_t num, uint32_t align) + { + assert(is_pow2(align)); + + uint32_t pad = -(uintptr_t) (base + num) & (align-1); + + if (used + pad + num > size) + return nullptr; + + void *ptr = base + used + pad; + used += pad + num; + + return ptr; + } +}; + +struct Scanner { + const char *src; + uint32_t cur; + uint32_t len; +}; + +struct Token { + enum Type { + ADD, + SUB, + MUL, + DIV, + INT, + FLOAT, + OTHER, + END, + }; + Type type; + union { + double _float; + int64_t _int; + } data; + + Token() + { + type = OTHER; + } + + Token(Type type_) + { + type = type_; + if (type == INT) + data._int = 0; + else + data._float = 0; + } + + Token(int64_t val) + { + type = INT; + data._int = val; + } + + Token(double val) + { + type = FLOAT; + data._float = val; + } +}; + +static bool is_space(char c) +{ + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; +} + +static bool is_digit(char c) +{ + return c >= '0' && c <= '9'; +} + +static Token tokenize(Scanner &s) +{ + while (s.cur < s.len && is_space(s.src[s.cur])) + s.cur++; + + if (s.cur == s.len) + return Token::END; + + if (is_digit(s.src[s.cur])) { + /* + * May be either an integer or a float. Peek after the first + * digit sequence to see if there's a dot + */ + uint32_t peek = s.cur; + while (peek < s.len && is_digit(s.src[peek])) + peek++; + bool no_dot = (peek == s.len || s.src[peek] != '.'); + + if (no_dot) { + + /* + * Parse integer token + */ + + int64_t buf = 0; + do { + int n = s.src[s.cur] - '0'; + + if (buf > (INT64_MAX - n) / 10) { + /* + * Overflow. Consume all remaining digits and return + * the maximum representable value. + */ + while (s.cur < s.len && is_digit(s.src[s.cur])) + s.cur++; + return INT64_MAX; + } + + buf = buf * 10 + n; + s.cur++; + } while (s.cur < s.len && is_digit(s.src[s.cur])); + + return buf; + + } else { + + /* + * Parse float token + */ + + double buf = 0; + + do { + int n = s.src[s.cur] - '0'; + buf = buf * 10 + n; + s.cur++; + } while (s.src[s.cur] != '.'); + + s.cur++; // Consume the dot + + /* + * Consume the decimal part, if present + */ + if (s.cur < s.len && is_digit(s.src[s.cur])) { + double q = 1; + do { + q /= 10; + int n = s.src[s.cur] - '0'; + buf += q * n; + s.cur++; + } while (s.cur < s.len && is_digit(s.src[s.cur])); + } + + return buf; + } + } + + if (s.src[s.cur] == '+') { + s.cur++; + return Token::ADD; + } + + if (s.src[s.cur] == '-') { + s.cur++; + return Token::SUB; + } + + if (s.src[s.cur] == '*') { + s.cur++; + return Token::MUL; + } + + if (s.src[s.cur] == '/') { + s.cur++; + return Token::DIV; + } + + /* + * Unexpected character + */ + return Token::OTHER; +} + +static int precedence_of(Token token) +{ + switch (token.type) { + case Token::ADD: + case Token::SUB: + return 0; + + case Token::MUL: + case Token::DIV: + return 1; + + default:break; + } + + return -1; +} + +static bool is_right_associative(Token op) +{ + (void) op; + return false; +} + +/* + * Returns true if the next token is an operator + * with a greater or equal precedence than the one + * specified. The following operator is stored in + * "op", but is only consimed when true is returned. + */ +static bool +follows_oper_of_higher_precedence(Scanner &s, Token &op, int min) +{ + uint32_t save = s.cur; + op = tokenize(s); + + if (precedence_of(op) >= min) + return true; + + s.cur = save; + return false; +} + +static bool should_associate_right(Scanner &s, Token op, Token &peek) +{ + uint32_t save = s.cur; + peek = tokenize(s); + s.cur = save; + + int p1 = precedence_of(op); + int p2 = precedence_of(peek); + + if (p1 < p2) + return true; + + if (p1 == p2 && is_right_associative(peek)) + return true; + + return false; +} + +static Expr *make_int(Arena &a, int64_t raw) +{ + void *p = a.alloc(sizeof(IntValue), alignof(IntValue)); + if (p == nullptr) + return nullptr; + + return new (p) IntValue(raw); +} + +static Expr *make_float(Arena &a, double raw) +{ + FloatValue *n; + + n = (FloatValue*) a.alloc(sizeof(FloatValue), alignof(FloatValue)); + if (n == nullptr) + return nullptr; + + new (n) FloatValue(raw); + return n; +} + +static Expr *make_neg(Arena &a, Expr *operand) +{ + NegOperation *n; + + n = (NegOperation*) a.alloc(sizeof(NegOperation), alignof(NegOperation)); + if (n == nullptr) + return nullptr; + + new (n) NegOperation(operand); + return n; +} + +static Expr *parse_primary(Scanner &s, Arena &a) +{ + Token t; + + /* + * Handle preceding unary operators + */ + bool neg = false; + for (;;) { + t = tokenize(s); + if (t.type == Token::SUB) + neg = !neg; + else { + if (t.type != Token::ADD) + break; + } + } + + Expr *e; + switch (t.type) { + + case Token::INT : e = make_int (a, t.data._int); break; + case Token::FLOAT : e = make_float(a, t.data._float); break; + + default: + case Token::END: + case Token::OTHER: + return nullptr; + } + if (e == nullptr) + return nullptr; + + if (neg) + return make_neg(a, e); // May return NULL + else + return e; +} + +static Expr *make_add(Arena &a, Expr *lhs, Expr *rhs) +{ + AddOperation *n; + + n = (AddOperation*) a.alloc(sizeof(AddOperation), alignof(AddOperation)); + if (n == nullptr) + return nullptr; + + new (n) AddOperation(lhs, rhs); + return n; +} + +static Expr *make_sub(Arena &a, Expr *lhs, Expr *rhs) +{ + SubOperation *n; + + n = (SubOperation*) a.alloc(sizeof(SubOperation), alignof(SubOperation)); + if (n == nullptr) + return nullptr; + + new (n) SubOperation(lhs, rhs); + return n; +} + +static Expr *make_mul(Arena &a, Expr *lhs, Expr *rhs) +{ + MulOperation *n; + + n = (MulOperation*) a.alloc(sizeof(MulOperation), alignof(MulOperation)); + if (n == nullptr) + return nullptr; + + new (n) MulOperation(lhs, rhs); + return n; +} + +static Expr *make_div(Arena &a, Expr *lhs, Expr *rhs) +{ + DivOperation *n; + + n = (DivOperation*) a.alloc(sizeof(DivOperation), alignof(DivOperation)); + if (n == nullptr) + return nullptr; + + new (n) DivOperation(lhs, rhs); + return n; +} + +/* + * Got this algorithm from: + * https://en.wikipedia.org/wiki/Operator-precedence_parser + */ +static Expr *parse_expression_2(Scanner &s, Arena &a, + Expr *lhs, int min_precedence) +{ + for (Token op; follows_oper_of_higher_precedence(s, op, min_precedence); ) { + + Expr *rhs = parse_primary(s, a); + if (rhs == nullptr) + return nullptr; + + for (Token peek; should_associate_right(s, op, peek); ) { + + int p1 = precedence_of(op); + int p2 = precedence_of(peek); + + rhs = parse_expression_2(s, a, rhs, p1 + (p2 > p1)); + if (rhs == nullptr) + return nullptr; + } + + switch (op.type) { + case Token::ADD: lhs = make_add(a, lhs, rhs); break; + case Token::SUB: lhs = make_sub(a, lhs, rhs); break; + case Token::MUL: lhs = make_mul(a, lhs, rhs); break; + case Token::DIV: lhs = make_div(a, lhs, rhs); break; + default:break; + } + if (lhs == nullptr) + return nullptr; + } + + return lhs; +} + +Expr *parse(const char *src, uint32_t len, + void *mem, uint32_t memlen) +{ + Arena a; + a.init(mem, memlen); + + Scanner s; + s.src = src; + s.len = len; + s.cur = 0; + + Expr *e; + + e = parse_primary(s, a); + if (e == nullptr) + return nullptr; + + return parse_expression_2(s, a, e, 0); +} diff --git a/parse.hpp b/parse.hpp new file mode 100644 index 0000000..53fb26c --- /dev/null +++ b/parse.hpp @@ -0,0 +1,4 @@ +#include "expr.hpp" + +Expr *parse(const char *src, uint32_t len, + void *mem, uint32_t memlen); diff --git a/stringify.cpp b/stringify.cpp new file mode 100644 index 0000000..49eb29c --- /dev/null +++ b/stringify.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include "expr.hpp" + +struct StringifyVisitor : Visitor { + + char *dst; + uint32_t num; + uint32_t max; + + void append(const char *str, uint32_t len) + { + if (num < max) { + uint32_t cpy = std::min(len, max - num); + memcpy(dst + num, str, cpy); + } + num += len; + } + + StringifyVisitor(char *dst_, uint32_t max_) + { + dst = dst_; + max = max_; + num = 0; + } + + void visit_int(IntValue *node) + { + std::ostringstream sstr; + sstr << node->val; + std::string str = sstr.str(); + append(str.c_str(), str.size()); + } + + void visit_float(FloatValue *node) + { + std::ostringstream sstr; + sstr << node->val; + std::string str = sstr.str(); + append(str.c_str(), str.size()); + } + + void visit_neg(NegOperation *node) + { + append("-", 1); + node->op->visit(*this); + } + + void visit_add(AddOperation *node) + { + append("(", 1); + node->lhs->visit(*this); + append(" + ", 3); + node->rhs->visit(*this); + append(")", 1); + } + + void visit_sub(SubOperation *node) + { + append("(", 1); + node->lhs->visit(*this); + append(" - ", 3); + node->rhs->visit(*this); + append(")", 1); + } + + void visit_mul(MulOperation *node) + { + append("(", 1); + node->lhs->visit(*this); + append(" * ", 3); + node->rhs->visit(*this); + append(")", 1); + } + + void visit_div(DivOperation *node) + { + append("(", 1); + node->lhs->visit(*this); + append(" / ", 3); + node->rhs->visit(*this); + append(")", 1); + } +}; + +uint32_t stringify(Expr *e, char *dst, uint32_t max) +{ + StringifyVisitor visitor(dst, max); + e->visit(visitor); + uint32_t num = visitor.num; + + if (max > 0) { + if (num == max) + num--; + dst[num] = '\0'; + } + + return num; +} diff --git a/stringify.hpp b/stringify.hpp new file mode 100644 index 0000000..202e493 --- /dev/null +++ b/stringify.hpp @@ -0,0 +1,3 @@ +#include "expr.hpp" + +uint32_t stringify(Expr *e, char *dst, uint32_t max); \ No newline at end of file