evalexpr: initial recursive descent parser
This commit is contained in:
commit
a92e39dbec
23
Makefile
Normal file
23
Makefile
Normal 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
67
src/ast/ast.c
Normal 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
61
src/ast/ast.h
Normal 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
69
src/eval/eval.c
Normal 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
8
src/eval/eval.h
Normal 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
33
src/evalexpr.c
Normal 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
267
src/parse/parse.c
Normal 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
8
src/parse/parse.h
Normal 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 */
|
Loading…
Reference in a new issue