evalexpr: initial recursive descent parser

This commit is contained in:
Bruno BELANYI 2020-10-28 15:30:54 +01:00
commit a92e39dbec
8 changed files with 536 additions and 0 deletions

23
Makefile Normal file
View file

@ -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

67
src/ast/ast.c Normal file
View file

@ -0,0 +1,67 @@
#include "ast.h"
#include <stdlib.h>
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);
}

61
src/ast/ast.h Normal file
View file

@ -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 */

69
src/eval/eval.c Normal file
View file

@ -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();
}

8
src/eval/eval.h Normal file
View file

@ -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 */

33
src/evalexpr.c Normal file
View file

@ -0,0 +1,33 @@
#include <stdio.h>
#include <stdlib.h>
#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;
}

267
src/parse/parse.c Normal file
View file

@ -0,0 +1,267 @@
#include "parse.h"
#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#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;
}

8
src/parse/parse.h Normal file
View file

@ -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 */