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