commit a15ca3add779596ec2bd5bd2207051739fbf5ec7 Author: Francesco Cozzuto Date: Sun Apr 7 17:55:16 2024 +0200 first commit 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