commit a92e39dbecd8ecc08e77d68d2bbf7153f97fdd34 Author: Bruno BELANYI Date: Wed Oct 28 15:30:54 2020 +0100 evalexpr: initial recursive descent parser diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8dfccac --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +CC = gcc +CPPFLAGS = -Isrc/ -D_POSIX_C_SOURCE=200809L +CFLAGS = -Wall -Wextra -pedantic -Werror -std=c99 +VPATH = src/ + +SRC = \ + src/ast/ast.c \ + src/eval/eval.c \ + src/parse/parse.c \ + +BIN = evalexpr +OBJ = $(SRC:.c=.o) + +.PHONY: all +all: $(BIN) + +# Write this one rule instead of using the implicit rules to buid at the root +$(BIN): $(OBJ) src/evalexpr.o + +.PHONY: clean +clean: + $(RM) $(OBJ) # remove object files + $(RM) $(BIN) # remove main program diff --git a/src/ast/ast.c b/src/ast/ast.c new file mode 100644 index 0000000..4db6bb8 --- /dev/null +++ b/src/ast/ast.c @@ -0,0 +1,67 @@ +#include "ast.h" + +#include + +struct ast_node *make_num(int val) +{ + struct ast_node *ret = malloc(sizeof(*ret)); + + if (ret == NULL) + return ret; + + ret->kind = NODE_NUM; + ret->val.num = val; + + return ret; +} + +struct ast_node *make_unop(enum unop_kind op, struct ast_node *tree) +{ + struct ast_node *ret = malloc(sizeof(*ret)); + + if (ret == NULL) + return ret; + + ret->kind = NODE_UNOP; + ret->val.un_op.op = op; + ret->val.un_op.tree = tree; + + return ret; +} + +struct ast_node *make_binop(enum binop_kind op, struct ast_node *lhs, + struct ast_node *rhs) +{ + struct ast_node *ret = malloc(sizeof(*ret)); + + if (ret == NULL) + return ret; + + ret->kind = NODE_BINOP; + ret->val.bin_op.op = op; + ret->val.bin_op.lhs = lhs; + ret->val.bin_op.rhs = rhs; + + return ret; +} + +void destroy_ast(struct ast_node *ast) +{ + if (!ast) + return; + + switch (ast->kind) + { + case NODE_BINOP: + destroy_ast(ast->val.bin_op.lhs); + destroy_ast(ast->val.bin_op.rhs); + break; + case NODE_UNOP: + destroy_ast(ast->val.un_op.tree); + /* fallthrough */ + case NODE_NUM: + break; + } + + free(ast); +} diff --git a/src/ast/ast.h b/src/ast/ast.h new file mode 100644 index 0000000..d992e62 --- /dev/null +++ b/src/ast/ast.h @@ -0,0 +1,61 @@ +#ifndef AST_H +#define AST_H + +// Forward declaration +struct ast_node; + +enum unop_kind +{ + UNOP_IDENTITY, + UNOP_NEGATE, + UNOP_FACT, +}; + +enum binop_kind +{ + BINOP_PLUS, + BINOP_MINUS, + BINOP_TIMES, + BINOP_DIVIDES, + BINOP_POW, +}; + +struct unop_node +{ + enum unop_kind op; + struct ast_node *tree; +}; + +struct binop_node +{ + enum binop_kind op; + struct ast_node *lhs; + struct ast_node *rhs; +}; + +struct ast_node +{ + enum node_kind + { + NODE_UNOP, + NODE_BINOP, + NODE_NUM, + } kind; + union ast_val + { + struct unop_node un_op; + struct binop_node bin_op; + int num; + } val; +}; + +struct ast_node *make_num(int val); + +struct ast_node *make_unop(enum unop_kind op, struct ast_node *tree); + +struct ast_node *make_binop(enum binop_kind op, struct ast_node *lhs, + struct ast_node *rhs); + +void destroy_ast(struct ast_node *ast); + +#endif /* !AST_H */ diff --git a/src/eval/eval.c b/src/eval/eval.c new file mode 100644 index 0000000..facb31a --- /dev/null +++ b/src/eval/eval.c @@ -0,0 +1,69 @@ +#include "eval.h" + +#define UNREACHABLE() __builtin_unreachable() + +static int my_pow(int lhs, int rhs) +{ + if (!rhs) + return 1; + int rec = my_pow(lhs * lhs, rhs / 2); + if (rhs & 1) + rec *= lhs; + return rec; +} + +static int my_fact(int num) +{ + int ret = 1; + while (num > 1) + ret *= num--; + return ret; +} + +static int eval_unop(const struct unop_node *un_op) +{ + switch (un_op->op) + { + case UNOP_IDENTITY: + return eval_ast(un_op->tree); + case UNOP_NEGATE: + return -eval_ast(un_op->tree); + case UNOP_FACT: + return my_fact(eval_ast(un_op->tree)); + } + UNREACHABLE(); +} + +static int eval_binop(const struct binop_node *bin_op) +{ +#define EVAL_OP(OP, TREE) (eval_ast((TREE)->lhs) OP eval_ast((TREE)->rhs)) + switch (bin_op->op) + { + case BINOP_PLUS: + return EVAL_OP(+, bin_op); + case BINOP_MINUS: + return EVAL_OP(-, bin_op); + case BINOP_TIMES: + return EVAL_OP(*, bin_op); + case BINOP_DIVIDES: + return EVAL_OP(/, bin_op); + case BINOP_POW: + return my_pow(eval_ast(bin_op->lhs), eval_ast(bin_op->rhs)); + } +#undef EVAL_OP + UNREACHABLE(); +} + +int eval_ast(const struct ast_node *ast) +{ + switch (ast->kind) + { + case NODE_NUM: + return ast->val.num; + case NODE_UNOP: + return eval_unop(&ast->val.un_op); + case NODE_BINOP: + return eval_binop(&ast->val.bin_op); + } + UNREACHABLE(); +} diff --git a/src/eval/eval.h b/src/eval/eval.h new file mode 100644 index 0000000..49d0a89 --- /dev/null +++ b/src/eval/eval.h @@ -0,0 +1,8 @@ +#ifndef EVAL_H +#define EVAL_H + +#include "ast/ast.h" + +int eval_ast(const struct ast_node *ast); + +#endif /* !EVAL_H */ diff --git a/src/evalexpr.c b/src/evalexpr.c new file mode 100644 index 0000000..15f1d1c --- /dev/null +++ b/src/evalexpr.c @@ -0,0 +1,33 @@ +#include +#include + +#include "ast/ast.h" +#include "eval/eval.h" +#include "parse/parse.h" + +int main(void) +{ + char *line = NULL; + size_t size = 0; + ssize_t ret = 0; + + while ((getline(&line, &size, stdin)) > 0) + { + struct ast_node *ast = parse_string(line); + + if (ast == NULL) + { + fputs("Could not parse input\n", stderr); + ret = 1; + continue; + } + + printf("%d\n", eval_ast(ast)); + + destroy_ast(ast); + } + + free(line); + + return ret; +} diff --git a/src/parse/parse.c b/src/parse/parse.c new file mode 100644 index 0000000..a644488 --- /dev/null +++ b/src/parse/parse.c @@ -0,0 +1,267 @@ +#include "parse.h" + +#include +#include +#include + +#include "ast/ast.h" + +#define UNREACHABLE() __builtin_unreachable() + +static struct ast_node *parse_expression(const char **input); +static struct ast_node *parse_term(const char **input); +static struct ast_node *parse_factor(const char **input); +static struct ast_node *parse_power(const char **input); +static struct ast_node *parse_group(const char **input); + +static void eat_char(const char **input) +{ + *input += 1; // Skip this character +} + +static void skip_whitespace(const char **input) +{ + while (*input[0] && isspace(*input[0])) + eat_char(input); +} + +/* +** Simple recursive descent using the following grammar, using E as start: +** +** E : T [ ('+'|'-') T ]* +** T : F [ ('*'|'/') F ]* +** F : [ ('-'|'+') ]* P +** P : G [ ('^') F ]* +** G : '(' E ')' | CONSTANT [ '!' ] +** +** Whitespace is ignored in the input string, only serving to delimit numbers. +** +** The input shall consist of a single expression, having a trailing +** expression in the input results in an error. +**/ + +struct ast_node *parse_string(const char *input) +{ + if (input == NULL) + return NULL; + + struct ast_node *ast = parse_expression(&input); + + if (ast == NULL) + return NULL; + + // Make sure there is no trailing character, except whitespace + skip_whitespace(&input); + if (input[0] != '\0') + { + destroy_ast(ast); + return NULL; + } + + return ast; +} + +static enum binop_kind char_to_binop(char c) +{ + switch (c) + { + case '+': + return BINOP_PLUS; + case '-': + return BINOP_MINUS; + case '*': + return BINOP_TIMES; + case '/': + return BINOP_DIVIDES; + case '^': + return BINOP_POW; + } + UNREACHABLE(); +} + +static struct ast_node *parse_expression(const char **input) +{ + struct ast_node *lhs = parse_term(input); + if (lhs == NULL) // Error occured, abort + return NULL; + + do + { + skip_whitespace(input); // Whitespace is not significant + + if (*input[0] == '\0') // End of input, return parsed expression + return lhs; + + if (*input[0] == '+' || *input[0] == '-') + { + const enum binop_kind op = char_to_binop(*input[0]); + + eat_char(input); + + struct ast_node *rhs = parse_term(input); + + if (rhs == NULL) // Error occured + { + destroy_ast(lhs); + return NULL; + } + + lhs = make_binop(op, lhs, rhs); + } + else + break; // Unexpected character, end of loop + } while (true); + + return lhs; +} + +static struct ast_node *parse_term(const char **input) +{ + struct ast_node *lhs = parse_factor(input); + if (lhs == NULL) // Error occured, abort + return NULL; + + do + { + skip_whitespace(input); // Whitespace is not significant + + if (*input[0] == '\0') // End of input, return parsed expression + return lhs; + + if (*input[0] == '*' || *input[0] == '/') + { + const enum binop_kind op = char_to_binop(*input[0]); + + eat_char(input); + + struct ast_node *rhs = parse_factor(input); + + if (rhs == NULL) // Error occured + { + destroy_ast(lhs); + return NULL; + } + + lhs = make_binop(op, lhs, rhs); + } + else + break; // Unexpected character, end of loop + } while (true); + + return lhs; +} + +static enum unop_kind char_to_unop(char c) +{ + switch (c) + { + case '+': // Assume non-faulty input, return identity as default + return UNOP_IDENTITY; + case '-': + return UNOP_NEGATE; + case '!': + return UNOP_FACT; + } + UNREACHABLE(); +} + +static struct ast_node *parse_factor(const char **input) +{ + skip_whitespace(input); // Whitespace is not significant + while (*input[0] == '+' || *input[0] == '-') + { + const enum unop_kind op = char_to_unop(*input[0]); + + eat_char(input); + + struct ast_node *rhs = parse_factor(input); // Loop by recursion + + if (rhs == NULL) + return NULL; + + return make_unop(op, rhs); + } + return parse_power(input); +} + +static struct ast_node *parse_power(const char **input) +{ + struct ast_node *lhs = parse_group(input); + if (lhs == NULL) // Error occured, abort + return NULL; + + skip_whitespace(input); // Whitespace is not significant + + if (*input[0] == '\0') // End of input, return parsed expression + return lhs; + + if (*input[0] == '^') + { + const enum binop_kind op = char_to_binop(*input[0]); + + eat_char(input); + + struct ast_node *rhs = parse_factor(input); + + if (rhs == NULL) // Error occured + { + destroy_ast(lhs); + return NULL; + } + + lhs = make_binop(op, lhs, rhs); + } + + return lhs; +} + +static bool my_atoi(const char **input, int *val) +{ + if (!isdigit(*input[0])) + return false; + + *val = 0; // Initialize its value + do + { + *val *= 10; + *val += *input[0] - '0'; + *input += 1; + } while (isdigit(*input[0])); + + return true; +} + +static struct ast_node *parse_group(const char **input) +{ + skip_whitespace(input); // Whitespace is not significant + struct ast_node *ast = NULL; + + int val = 0; + if (my_atoi(input, &val)) + ast = make_num(val); + else if (*input[0] == '(') + { + // Remove the parenthesis + eat_char(input); + ast = parse_expression(input); + // Check that we have our closing parenthesis + skip_whitespace(input); + if (*input[0] != ')') + { + destroy_ast(ast); + return NULL; + } + // Remove the parenthesis + eat_char(input); + return ast; + } + + skip_whitespace(input); + if (*input[0] == '!') + { + eat_char(input); + return make_unop(UNOP_FACT, ast); + } + + return ast; +} diff --git a/src/parse/parse.h b/src/parse/parse.h new file mode 100644 index 0000000..0121bc7 --- /dev/null +++ b/src/parse/parse.h @@ -0,0 +1,8 @@ +#ifndef PARSE_H +#define PARSE_H + +#include "ast/ast.h" + +struct ast_node *parse_string(const char *input); + +#endif /* !PARSE_H */