Compare commits

...

76 Commits

Author SHA1 Message Date
Karma Riuk
47379c6635 implemented simple evaluator for program,
statements and integer literals, together with
tests
2025-07-19 15:03:38 +02:00
Karma Riuk
7b01840f4d moved some stuff around in an attempt to make
things cleaner (not sure it is, but we tryin)
2025-07-19 13:41:25 +02:00
Karma Riuk
c943380d58 moved the utils for the parser to a global utils
folder for the tests (that utils is includable
only by the tests and not the src code, I added a
compiler flag only for the tests in the makefile,
but the compiler_flags.txt is global for the lsp,
gotta be careful with that)
2025-07-19 13:27:25 +02:00
Karma Riuk
d1dd5f9dab made our wrapper objects for all of our primitve
datatypes: ints, bools and null
2025-07-19 13:04:30 +02:00
Karma Riuk
c841cfe680 made the RLPL (read-lex-print-loop) become a
RPPL (read-parse-print-loop)
2025-07-15 20:21:53 +02:00
Karma Riuk
20b2c4a818 added checks for operator precedence with function
call
2025-07-15 20:09:08 +02:00
Karma Riuk
2b811a4bb9 added tests for malformed function call to check
for memory safety
2025-07-15 20:00:30 +02:00
Karma Riuk
a016bbaa5e added parsing of function call 2025-07-15 19:42:43 +02:00
Karma Riuk
59bff59cf7 renamed function for parsing function literal
to avoid confusion with parsing function calls
2025-07-15 09:25:00 +02:00
Karma Riuk
98fb161bba added macro for debugging 2025-07-15 01:43:39 +02:00
Karma Riuk
6313027d07 reformulated the parsing of the parameters and
made it memory safe
2025-07-15 01:43:22 +02:00
Karma Riuk
4a9deff8da extracted the parsing of the function parameters
into it's own function (will be useful for the
parsing of the parameters in the function calls)
2025-07-15 01:18:00 +02:00
Karma Riuk
e2dfca2679 renamed function->block to function->body 2025-07-15 01:13:52 +02:00
Karma Riuk
c6daa5c2af added tests for malformed function literal to
check for memory leaks
2025-07-15 01:11:53 +02:00
Karma Riuk
afbabd6c9e added check for corner case 2025-07-15 01:11:46 +02:00
Karma Riuk
057d6fc9f1 renamed subcase properly 2025-07-15 00:58:49 +02:00
Karma Riuk
0b2a12cef7 added parsing for function literals 2025-07-15 00:56:40 +02:00
Karma Riuk
85f530f5f9 made the parsing functions for the different nodes
return the type they are parsing (necessary for
the parsing of the identifiers while parsing the
list of parameteres in a function literal)
2025-07-15 00:24:53 +02:00
Karma Riuk
586a172d90 removed sus blank line 2025-07-15 00:07:39 +02:00
Karma Riuk
767a2ebcb1 added tests for malformed if to check of memory
leaks
2025-07-15 00:05:48 +02:00
Karma Riuk
d94bb99381 fixed implementation of parse block 2025-07-15 00:04:58 +02:00
Karma Riuk
7c55e58e1a removed captures that don't capture because of
scope issues with doctest
2025-07-14 23:56:51 +02:00
Karma Riuk
cbafb2e814 extracted and abstracted function 2025-07-14 20:15:51 +02:00
Karma Riuk
f0f748f7f5 uncommented tracers and using a macro to enable it
or disable it
2025-07-14 20:15:18 +02:00
Karma Riuk
1cda075f57 added guards for freeing pointers because it
caused seg faults
2025-07-14 20:14:54 +02:00
Karma Riuk
97b81e6771 fixed mistake that caused undefined behaviour 2025-07-14 20:14:39 +02:00
Karma Riuk
870567ec6b removed dereferencing pointers when outputing to
stdout because i wanted the option of seeing
wethere a pointer was null or not
2025-07-14 20:14:03 +02:00
Karma Riuk
34e4bfe9c9 fixed some memory leaks 2025-07-14 15:20:30 +02:00
Karma Riuk
9e63c923da added parsing of if statements 2025-07-14 15:19:05 +02:00
Karma Riuk
86574552aa added parsing and testing for grouped expressions 2025-07-12 15:22:45 +02:00
Karma Riuk
ab17545c93 made another test for boolean parsing in general
expression
2025-07-12 15:07:47 +02:00
Karma Riuk
e170afc840 used overloaded function to make code cleaner 2025-07-11 22:17:01 +02:00
Karma Riuk
aaec4cefcf added prefix parsing for boolean literals 2025-07-11 21:00:11 +02:00
Karma Riuk
4da5f32aea added boolean literals and tests for them (no
parsing yet)
2025-07-11 20:55:02 +02:00
Karma Riuk
8c1f4e10cd made more helper functions for testing 2025-07-11 20:34:18 +02:00
Karma Riuk
6cd99c22fe added a way to show the call stack of the functions 2025-07-11 19:48:55 +02:00
Karma Riuk
c9ffeafd5f added extensive testing for operator precedence 2025-07-11 11:39:55 +02:00
Karma Riuk
a1192204b1 fixed slight bug 2025-07-11 11:39:48 +02:00
Karma Riuk
7192bb318c fixed the to string function of some nodes 2025-07-11 11:39:45 +02:00
Karma Riuk
552e68169f fixed compiler warning 2025-07-11 11:21:45 +02:00
Karma Riuk
63f22a1b40 removed unused include 2025-07-11 11:19:28 +02:00
Karma Riuk
2174781b77 using smart pointers instead of normal ones for
easier setup of tests (to call setup() multiple
times without leaks)
2025-07-11 11:16:16 +02:00
Karma Riuk
826f4de77a updated return tests 2025-07-11 11:10:29 +02:00
Karma Riuk
a7f5950a55 implemented infix operator parsing 2025-07-11 10:30:37 +02:00
Karma Riuk
6e471a91d5 fixed test 2025-07-11 10:30:28 +02:00
Karma Riuk
1ec438c900 made infix string better 2025-07-11 09:12:42 +02:00
Karma Riuk
702c34a736 written tests for infix expressions 2025-07-11 09:07:33 +02:00
Karma Riuk
f038d30d77 written the test in a different way, since we
can't use for loops around subcases
2025-07-11 09:07:12 +02:00
Karma Riuk
ed3cf748e2 properly deallocating the prefix obeject 2025-07-11 09:01:09 +02:00
Karma Riuk
c55c6b2b20 forgot that statement and expression had a common
ancestor
2025-07-11 08:45:15 +02:00
Karma Riuk
7f1cc6f45e can now parse prefix expressions! 2025-07-09 12:02:01 +02:00
Karma Riuk
c7a30a0028 made casting issues log an even better message 2025-07-09 12:01:09 +02:00
Karma Riuk
2ff6c695f7 removed useless import 2025-07-09 12:00:57 +02:00
Karma Riuk
3ec6667ba3 improved logging errors 2025-07-09 11:46:47 +02:00
Karma Riuk
c65cefe867 extracted to a function the casting of the
different types to make tests cleaner
2025-07-09 11:07:21 +02:00
Karma Riuk
79b1aeb45f can now parse identifiers and integer literals 2025-07-09 10:16:13 +02:00
Karma Riuk
e3cbba08b1 fixed tests 2025-07-08 18:08:17 +02:00
Karma Riuk
74c555bfc0 added possibility of printing nodes 2025-07-08 18:05:52 +02:00
Karma Riuk
96d76153d4 added option for subcase 2025-07-08 18:05:29 +02:00
Karma Riuk
8fb0ef2be7 added quotes to avoid any cli issue 2025-07-08 17:23:34 +02:00
Karma Riuk
c9e21213fd made setup for expression parsing 2025-07-08 15:52:40 +02:00
Karma Riuk
d10e5676c1 better use of fixtures for resource allocation and
dealocation
2025-07-08 15:45:38 +02:00
Karma Riuk
83df4955d4 added tests for the stringification of the program 2025-07-08 11:18:54 +02:00
Karma Riuk
902f5a16df using fixtures (not super necessary, but nice) 2025-07-08 10:58:51 +02:00
Karma Riuk
2dfff61346 using test suites 2025-07-08 10:48:35 +02:00
Karma Riuk
ef624de4ef a little bit of cleanup 2025-07-08 10:20:10 +02:00
Karma Riuk
31cb483602 added string function to all nodes of ast 2025-07-08 10:17:38 +02:00
Karma Riuk
da2b6716b1 moved program to it's own files 2025-07-08 09:59:11 +02:00
Karma Riuk
1d259e6988 forgot pragma once 2025-07-08 09:56:47 +02:00
Karma Riuk
d13f9bf9f8 added the parsing of expression statements 2025-07-08 09:51:15 +02:00
Karma Riuk
08aacf0416 extracted the parsing of the "expressions" to it's
own function so we can just modify that once we
get there
2025-07-08 09:50:41 +02:00
Karma Riuk
0b9d7d9c33 added return parsing 2025-07-07 18:34:46 +02:00
Karma Riuk
1638ddbfa1 fixed tests 2025-07-07 18:33:27 +02:00
Karma Riuk
b98424aa5f renamed let to let_stmt 2025-07-07 17:44:11 +02:00
Karma Riuk
39eafe2360 made tests for return statements 2025-07-07 17:39:07 +02:00
Karma Riuk
7b916b2a0b created the parser dir test to avoid having a
bunch of "unrelated" tests in one file
2025-07-07 17:20:21 +02:00
62 changed files with 2569 additions and 317 deletions

View File

@@ -3,6 +3,7 @@
# ------------------------------------------------------------------- # -------------------------------------------------------------------
CXX := g++ CXX := g++
CXXFLAGS := -std=c++17 -Wall -Wextra -Iinclude -Isrc -MMD -MP CXXFLAGS := -std=c++17 -Wall -Wextra -Iinclude -Isrc -MMD -MP
CXXFLAGS_TESTS := -Itest/utils
LDFLAGS := LDFLAGS :=
SRC_DIR := src SRC_DIR := src
TEST_DIR := test TEST_DIR := test
@@ -48,7 +49,7 @@ run: $(TARGET)
@$(TARGET) @$(TARGET)
test: $(TEST_TARGET) test: $(TEST_TARGET)
@$(TEST_TARGET) $(if $(TEST),--test-case=$(TEST)) @$(TEST_TARGET) $(if $(TEST),--test-case="$(TEST)") $(if $(SUBCASE),--subcase="$(SUBCASE)")
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# Link binaries # Link binaries
@@ -70,7 +71,7 @@ $(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(OBJ_DIR)/test/%.o: $(TEST_DIR)/%.cpp $(OBJ_DIR)/test/%.o: $(TEST_DIR)/%.cpp
@mkdir -p $(@D) @mkdir -p $(@D)
$(CXX) $(CXXFLAGS) -c $< -o $@ $(CXX) $(CXXFLAGS) $(CXXFLAGS_TESTS) -c $< -o $@
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# Autoinclude dependencies # Autoinclude dependencies

View File

@@ -1,2 +1,3 @@
-I./include -I./include
-I./src -I./src
-I./test/utils

View File

@@ -1,9 +0,0 @@
#include "ast.hpp"
namespace ast {
std::string program ::token_literal() const {
if (statements.size() > 0)
return statements[0]->token_literal();
return "";
}
} // namespace ast

View File

@@ -1,29 +1,20 @@
#pragma once #pragma once
#include <iostream>
#include <string> #include <string>
#include <vector>
namespace ast { namespace ast {
struct node { struct node {
virtual std::string token_literal() const = 0; virtual std::string token_literal() const = 0;
virtual std::string str() const = 0;
virtual ~node() = default; virtual ~node() = default;
}; };
struct statement : node { inline std::ostream& operator<<(std::ostream& os, const node& n) {
virtual std::string token_literal() const override = 0; return os << n.str();
}; }
struct expression : node { struct statement : node {};
virtual std::string token_literal() const override = 0;
};
struct program : public node { struct expression : node {};
std::vector<statement*> statements;
std::string token_literal() const override;
~program() {
for (const auto& ref : statements)
delete ref;
};
};
} // namespace ast } // namespace ast

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "token/token.hpp"
#include "token/type.hpp" #include "token/type.hpp"
namespace ast::error { namespace ast::error {
@@ -12,6 +13,14 @@ namespace ast::error {
explicit parser_error(const std::string& message): error(message) {} explicit parser_error(const std::string& message): error(message) {}
}; };
struct unkown_prefix : parser_error {
token::token prefix;
explicit unkown_prefix(token::token prefix, const std::string& message)
: parser_error(message),
prefix(prefix) {}
};
struct expected_next : parser_error { struct expected_next : parser_error {
token::type expected_type; token::type expected_type;

View File

@@ -0,0 +1,15 @@
#include "boolean.hpp"
namespace ast {
boolean_literal::boolean_literal(token::token token, bool value)
: token(std::move(token)),
value(value) {}
std::string boolean_literal::token_literal() const {
return token.literal;
}
std::string boolean_literal::str() const {
return token.literal;
};
} // namespace ast

View File

@@ -0,0 +1,17 @@
#pragma once
#include "ast/ast.hpp"
#include "token/token.hpp"
#include <string>
namespace ast {
struct boolean_literal : expression {
boolean_literal(token::token, bool);
token::token token;
bool value;
std::string token_literal() const override;
std::string str() const override;
};
} // namespace ast

View File

@@ -0,0 +1,66 @@
#include "function.hpp"
#include <sstream>
namespace ast {
function_literal::function_literal(token::token token)
: token(std::move(token)),
body(nullptr) {};
std::string function_literal::token_literal() const {
return token.literal;
};
std::string function_literal::str() const {
std::stringstream ss;
ss << "fn (";
bool first = true;
for (auto& param : parameters) {
if (!first)
ss << ", ";
ss << param->str();
first = false;
}
ss << ")";
ss << body->str();
return ss.str();
};
function_literal::~function_literal() {
for (auto& param : parameters)
delete param;
if (body != nullptr)
delete body;
}
// ---------------------------- FUNCTION CALL ----------------------------
function_call::function_call(token::token token, expression* target)
: token(std::move(token)),
target(target) {};
std::string function_call::token_literal() const {
return token.literal;
};
std::string function_call::str() const {
std::stringstream ss;
ss << target->str() << "(";
bool first = true;
for (auto& param : parameters) {
if (!first)
ss << ", ";
ss << param->str();
first = false;
}
ss << ")";
return ss.str();
};
function_call::~function_call() {
if (target != nullptr)
delete target;
for (auto& param : parameters)
delete param;
}
} // namespace ast

View File

@@ -0,0 +1,32 @@
#pragma once
#include "ast/ast.hpp"
#include "ast/expressions/identifier.hpp"
#include "ast/statements/block.hpp"
#include "token/token.hpp"
#include <string>
namespace ast {
struct function_literal : expression {
function_literal(token::token);
token::token token;
std::vector<identifier*> parameters;
ast::block_stmt* body;
std::string token_literal() const override;
std::string str() const override;
~function_literal();
};
struct function_call : expression {
function_call(token::token, expression*);
token::token token;
expression* target;
std::vector<expression*> parameters;
std::string token_literal() const override;
std::string str() const override;
~function_call();
};
} // namespace ast

View File

@@ -8,4 +8,8 @@ namespace ast {
std::string identifier::token_literal() const { std::string identifier::token_literal() const {
return token.literal; return token.literal;
} }
std::string identifier::str() const {
return value;
};
} // namespace ast } // namespace ast

View File

@@ -12,5 +12,6 @@ namespace ast {
std::string value; std::string value;
std::string token_literal() const override; std::string token_literal() const override;
std::string str() const override;
}; };
} // namespace ast } // namespace ast

View File

@@ -0,0 +1,32 @@
#include "if_then_else.hpp"
#include <sstream>
namespace ast {
if_then_else::if_then_else(token::token token)
: token(std::move(token)),
condition(nullptr),
consequence(nullptr),
alternative(nullptr) {};
std::string if_then_else::token_literal() const {
return token.literal;
};
std::string if_then_else::str() const {
std::stringstream ss;
ss << "if " << condition->str() << consequence->str();
if (alternative != nullptr)
ss << "else" << alternative->str();
return ss.str();
};
if_then_else::~if_then_else() {
if (condition != nullptr)
delete condition;
if (consequence != nullptr)
delete consequence;
if (alternative != nullptr)
delete alternative;
}
} // namespace ast

View File

@@ -0,0 +1,20 @@
#pragma once
#include "ast/ast.hpp"
#include "ast/statements/block.hpp"
#include "token/token.hpp"
#include <string>
namespace ast {
struct if_then_else : expression {
if_then_else(token::token);
token::token token;
ast::expression* condition;
ast::block_stmt *consequence, *alternative;
std::string token_literal() const override;
std::string str() const override;
~if_then_else();
};
} // namespace ast

View File

@@ -0,0 +1,26 @@
#include "infix.hpp"
#include "token/token.hpp"
namespace ast {
infix_expr::infix_expr(
token::token token, std::string op, ast::expression* left
)
: token(std::move(token)),
op(op),
left(left) {}
std::string infix_expr::token_literal() const {
return token.literal;
}
std::string infix_expr::str() const {
return "(" + left->str() + " " + op + " " + right->str() + ")";
}
infix_expr::~infix_expr() {
delete left;
delete right;
};
} // namespace ast

View File

@@ -0,0 +1,18 @@
#pragma once
#include "ast/ast.hpp"
#include "token/token.hpp"
namespace ast {
struct infix_expr : expression {
infix_expr(token::token, std::string, ast::expression*);
token::token token;
std::string op;
expression* left;
expression* right;
std::string token_literal() const override;
std::string str() const override;
~infix_expr();
};
} // namespace ast

View File

@@ -0,0 +1,15 @@
#include "integer.hpp"
namespace ast {
integer_literal::integer_literal(token::token token, int value)
: token(std::move(token)),
value(value) {}
std::string integer_literal::token_literal() const {
return token.literal;
};
std::string integer_literal::str() const {
return token.literal;
};
} // namespace ast

View File

@@ -0,0 +1,15 @@
#pragma once
#include "ast/ast.hpp"
#include "token/token.hpp"
namespace ast {
struct integer_literal : expression {
integer_literal(token::token token, int value);
token::token token;
int value;
std::string token_literal() const override;
std::string str() const override;
};
} // namespace ast

View File

@@ -0,0 +1,23 @@
#include "prefix.hpp"
#include "token/token.hpp"
namespace ast {
prefix_expr::prefix_expr(token::token token, std::string op)
: token(std::move(token)),
op(op),
right(nullptr) {}
std::string prefix_expr::token_literal() const {
return token.literal;
}
std::string prefix_expr::str() const {
return "(" + op + right->str() + ")";
}
prefix_expr::~prefix_expr() {
delete right;
};
} // namespace ast

View File

@@ -0,0 +1,17 @@
#pragma once
#include "ast/ast.hpp"
#include "token/token.hpp"
namespace ast {
struct prefix_expr : expression {
prefix_expr(token::token token, std::string op);
token::token token;
std::string op;
expression* right;
std::string token_literal() const override;
std::string str() const override;
~prefix_expr();
};
} // namespace ast

24
src/ast/program.cpp Normal file
View File

@@ -0,0 +1,24 @@
#include "program.hpp"
#include <sstream>
namespace ast {
std::string program ::token_literal() const {
if (statements.size() > 0)
return statements[0]->token_literal();
return "";
}
program::~program() {
for (const auto& ref : statements)
delete ref;
};
std::string program::str() const {
std::stringstream ss;
for (const auto& stmt : statements)
ss << stmt->str();
return ss.str();
};
} // namespace ast

23
src/ast/program.hpp Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include "ast.hpp"
#include <string>
#include <vector>
namespace ast {
struct program : public node {
std::vector<statement*> statements;
program() {}
program(std::vector<statement*> statements)
: statements(std::move(statements)) {};
std::string token_literal() const override;
virtual std::string str() const override;
~program();
};
} // namespace ast

View File

@@ -0,0 +1,25 @@
#include "block.hpp"
#include <sstream>
namespace ast {
block_stmt::block_stmt(token::token token): token(std::move(token)) {}
std::string block_stmt::token_literal() const {
return token.literal;
}
block_stmt::~block_stmt() {
for (const auto& stmt : statements)
delete stmt;
};
std::string block_stmt::str() const {
std::stringstream ss;
ss << "{";
for (const auto& stmt : statements)
ss << stmt->str();
ss << "}";
return ss.str();
};
} // namespace ast

View File

@@ -0,0 +1,20 @@
#pragma once
#include "ast/ast.hpp"
#include "token/token.hpp"
#include <vector>
namespace ast {
struct block_stmt : statement {
block_stmt(token::token token);
token::token token;
std::vector<ast::statement*> statements;
std::string token_literal() const override;
std::string str() const override;
~block_stmt();
};
} // namespace ast

View File

@@ -0,0 +1,27 @@
#include "expression.hpp"
#include <sstream>
namespace ast {
expression_stmt::expression_stmt(token::token token)
: token(std::move(token)),
expression(nullptr) {}
std::string expression_stmt::token_literal() const {
return token.literal;
}
expression_stmt::~expression_stmt() {
delete expression;
}
std::string expression_stmt::str() const {
std::stringstream ss;
if (expression != nullptr)
ss << expression->str();
return ss.str();
};
} // namespace ast

View File

@@ -0,0 +1,18 @@
#pragma once
#include "ast/ast.hpp"
#include "token/token.hpp"
namespace ast {
struct expression_stmt : statement {
expression_stmt(token::token token);
token::token token;
ast::expression* expression;
std::string token_literal() const override;
std::string str() const override;
~expression_stmt();
};
} // namespace ast

View File

@@ -1,17 +1,36 @@
#include "let.hpp" #include "let.hpp"
#include <sstream>
namespace ast { namespace ast {
let::let(token::token token) let_stmt::let_stmt(token::token token)
: token(std::move(token)), : token(std::move(token)),
name(nullptr), name(nullptr),
value(nullptr) {} value(nullptr) {}
std::string let::token_literal() const { let_stmt::let_stmt(token::token token, identifier* name, expression* value)
: token(std::move(token)),
name(name),
value(value) {}
std::string let_stmt::token_literal() const {
return token.literal; return token.literal;
} }
let::~let() { let_stmt::~let_stmt() {
delete name; delete name;
delete value; delete value;
}; };
std::string let_stmt::str() const {
std::stringstream ss;
ss << token_literal() << ' ' << name->str() << " = ";
if (value != nullptr)
ss << value->str();
ss << ';';
return ss.str();
};
} // namespace ast } // namespace ast

View File

@@ -5,15 +5,17 @@
#include "token/token.hpp" #include "token/token.hpp"
namespace ast { namespace ast {
struct let : statement { struct let_stmt : statement {
let(token::token token); let_stmt(token::token token);
let_stmt(token::token token, identifier* name, expression* value);
token::token token; token::token token;
identifier* name; identifier* name;
expression* value; expression* value;
std::string token_literal() const override; std::string token_literal() const override;
std::string str() const override;
~let(); ~let_stmt();
}; };
} // namespace ast } // namespace ast

View File

@@ -0,0 +1,33 @@
#include "return.hpp"
#include <sstream>
namespace ast {
return_stmt::return_stmt(token::token token)
: token(std::move(token)),
value(nullptr) {}
return_stmt::return_stmt(token::token token, expression* value)
: token(std::move(token)),
value(value) {}
std::string return_stmt::token_literal() const {
return token.literal;
}
return_stmt::~return_stmt() {
delete value;
};
std::string return_stmt::str() const {
std::stringstream ss;
ss << token_literal() << " ";
if (value != nullptr)
ss << value->str();
ss << ';';
return ss.str();
};
} // namespace ast

View File

@@ -0,0 +1,19 @@
#pragma once
#include "ast/ast.hpp"
#include "token/token.hpp"
namespace ast {
struct return_stmt : statement {
return_stmt(token::token token);
return_stmt(token::token token, expression* value);
token::token token;
expression* value;
std::string token_literal() const override;
std::string str() const override;
~return_stmt();
};
} // namespace ast

View File

@@ -0,0 +1,41 @@
#include "evaluator.hpp"
#include "ast/ast.hpp"
#include "ast/expressions/boolean.hpp"
#include "ast/expressions/integer.hpp"
#include "ast/program.hpp"
#include "ast/statements/expression.hpp"
#include "object/boolean.hpp"
#include "object/integers.hpp"
#include "object/null.hpp"
#include <vector>
namespace eval {
static object::null* null = new object::null;
object::object* eval(std::vector<ast::statement*> statements) {
object::object* ret;
for (auto& stmt : statements)
ret = eval(stmt);
return ret;
}
object::object* eval(ast::node* node) {
if (ast::integer_literal* integer =
dynamic_cast<ast::integer_literal*>(node)) {
return new object::integer(integer->value);
} else if (ast::boolean_literal* boolean =
dynamic_cast<ast::boolean_literal*>(node)) {
return new object::boolean(boolean->value);
} else if (ast::program* program = dynamic_cast<ast::program*>(node)) {
return eval(program->statements);
} else if (ast::expression_stmt* expression_stmt =
dynamic_cast<ast::expression_stmt*>(node)) {
return eval(expression_stmt->expression);
}
return null;
}
} // namespace eval

View File

@@ -0,0 +1,8 @@
#pragma once
#include "ast/ast.hpp"
#include "object/object.hpp"
namespace eval {
object::object* eval(ast::node*);
} // namespace eval

View File

@@ -7,6 +7,8 @@
#include <iostream> #include <iostream>
namespace lexer { namespace lexer {
lexer::lexer(std::istream& input): input(input) {}
token::token lexer::next_token() { token::token lexer::next_token() {
if (!(input >> c)) if (!(input >> c))
return {token::type::END_OF_FILE, ""}; return {token::type::END_OF_FILE, ""};

View File

@@ -1,10 +1,12 @@
#pragma once #pragma once
#include "token/token.hpp" #include "token/token.hpp"
#include <istream> #include <istream>
namespace lexer { namespace lexer {
struct lexer { struct lexer {
lexer(std::istream&);
std::istream& input; std::istream& input;
char c = 0; char c = 0;
token::token next_token(); token::token next_token();

9
src/object/boolean.cpp Normal file
View File

@@ -0,0 +1,9 @@
#include "boolean.hpp"
#include <string>
namespace object {
std::string boolean::inspect() {
return std::to_string(value);
}
} // namespace object

13
src/object/boolean.hpp Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include "object.hpp"
namespace object {
struct boolean : object {
bool value;
::object::type type = type::BOOLEAN_OBJ;
explicit boolean(bool value): value(value) {}
std::string inspect();
};
} // namespace object

9
src/object/integers.cpp Normal file
View File

@@ -0,0 +1,9 @@
#include "integers.hpp"
#include <string>
namespace object {
std::string integer::inspect() {
return std::to_string(value);
}
} // namespace object

13
src/object/integers.hpp Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include "object.hpp"
namespace object {
struct integer : object {
int value;
::object::type type = type::INTEGER_OBJ;
explicit integer(int value): value(value) {}
std::string inspect();
};
} // namespace object

7
src/object/null.cpp Normal file
View File

@@ -0,0 +1,7 @@
#include "null.hpp"
namespace object {
std::string null::inspect() {
return "null";
}
} // namespace object

10
src/object/null.hpp Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
#include "object.hpp"
namespace object {
struct null : object {
::object::type type = type::NULL_OBJ;
std::string inspect();
};
} // namespace object

13
src/object/object.hpp Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include "type.hpp"
#include <string>
namespace object {
struct object {
::object::type type;
virtual std::string inspect() = 0;
};
} // namespace object

23
src/object/type.cpp Normal file
View File

@@ -0,0 +1,23 @@
#include "type.hpp"
#include <array>
namespace object {
// Array mapping enum values to their string representations
constexpr std::
array<std::string_view, static_cast<size_t>(type::BOOLEAN_OBJ) + 1>
tokenTypeStrings = {
#define X(name, str) str,
OBJECT_LIST
#undef X
};
// Stream insertion operator using the lookup array
std::ostream& operator<<(std::ostream& os, type type) {
auto idx = static_cast<size_t>(type);
if (idx < tokenTypeStrings.size())
return os << tokenTypeStrings[idx];
return os << "Unknown";
}
} // namespace object

21
src/object/type.hpp Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include <ostream>
namespace object {
// X-macro list of token types and their string representations
#define OBJECT_LIST \
X(NULL_OBJ, "NULL") \
X(INTEGER_OBJ, "INTEGER") \
X(BOOLEAN_OBJ, "BOOLEAN")
// Define the TokenType enum using the X-macro
enum class type {
#define X(name, str) name,
OBJECT_LIST
#undef X
};
std::ostream& operator<<(std::ostream&, type);
} // namespace object

View File

@@ -1,10 +1,24 @@
#include "parser.hpp" #include "parser.hpp"
#include "ast/errors/error.hpp" #include "ast/errors/error.hpp"
#include "ast/expressions/boolean.hpp"
#include "ast/expressions/function.hpp"
#include "ast/expressions/identifier.hpp"
#include "ast/expressions/if_then_else.hpp"
#include "ast/expressions/infix.hpp"
#include "ast/expressions/integer.hpp"
#include "ast/expressions/prefix.hpp"
#include "ast/statements/block.hpp"
#include "token/token.hpp"
#include "token/type.hpp" #include "token/type.hpp"
#include "utils/tracer.hpp"
#include <sstream> #include <sstream>
#define LOG_CUR_NEXT \
std::cout << "current: " << current << std::endl; \
std::cout << "next: " << next << std::endl;
namespace parser { namespace parser {
parser::parser(lexer::lexer& lexer) parser::parser(lexer::lexer& lexer)
: lexer(lexer), : lexer(lexer),
@@ -12,6 +26,65 @@ namespace parser {
next(token::type::ILLEGAL, "") { next(token::type::ILLEGAL, "") {
next_token(); next_token();
next_token(); next_token();
register_prefix(
token::type::IDENTIFIER,
std::bind(&parser::parse_identifier, this)
);
register_prefix(
token::type::INT,
std::bind(&parser::parse_integer, this)
);
register_prefix(
{
token::type::BANG,
token::type::MINUS,
},
std::bind(&parser::parse_prefix_expr, this)
);
register_prefix(
{
token::type::TRUE,
token::type::FALSE,
},
std::bind(&parser::parse_boolean, this)
);
register_prefix(
token::type::LPAREN,
std::bind(&parser::parse_grouped_expr, this)
);
register_prefix(
token::type::IF,
std::bind(&parser::parse_if_then_else, this)
);
register_prefix(
token::type::FUNCTION,
std::bind(&parser::parse_function_lit, this)
);
using namespace std::placeholders;
register_infix(
{token::type::PLUS,
token::type::MINUS,
token::type::ASTERISK,
token::type::SLASH,
token::type::EQ,
token::type::NEQ,
token::type::GT,
token::type::LT},
std::bind(&parser::parse_infix_expr, this, _1)
);
register_infix(
token::type::LPAREN,
std::bind(&parser::parse_function_call, this, _1)
);
} }
void parser::next_token() { void parser::next_token() {
@@ -19,8 +92,14 @@ namespace parser {
next = lexer.next_token(); next = lexer.next_token();
} }
ast::program* parser::parse_program() { void parser::skip_until_semicolon() {
ast::program* p = new ast::program(); for (; current.type != token::type::SEMICOLON
&& current.type != token::type::END_OF_FILE;
next_token()) {};
}
std::unique_ptr<ast::program> parser::parse_program() {
std::unique_ptr<ast::program> p = std::make_unique<ast::program>();
for (; current.type != token::type::END_OF_FILE; next_token()) { for (; current.type != token::type::END_OF_FILE; next_token()) {
ast::statement* stmt = parse_statement(); ast::statement* stmt = parse_statement();
@@ -35,10 +114,83 @@ namespace parser {
switch (current.type) { switch (current.type) {
case token::type::LET: case token::type::LET:
return parse_let(); return parse_let();
case token::type::RETURN:
return parse_return();
default: default:
return parse_expression_stmt();
}
}
ast::expression* parser::parse_expression(precedence prec) {
TRACE_FUNCTION;
auto prefix_it = prefix_parse_fns.find(current.type);
if (prefix_it == prefix_parse_fns.end()) {
unkown_prefix_error(current);
return nullptr; return nullptr;
} }
prefix_parse_fn prefix = prefix_it->second;
ast::expression* left = prefix();
while (next.type != token::type::SEMICOLON
&& prec < precedence_for(next.type)) {
auto infix_it = infix_parse_fns.find(next.type);
if (infix_it == infix_parse_fns.end())
return left;
next_token();
infix_parse_fn infix = infix_it->second;
left = infix(left);
} }
return left;
};
ast::return_stmt* parser::parse_return() {
ast::return_stmt* stmt = new ast::return_stmt(current);
next_token();
stmt->value = parse_expression();
if (next.type == token::type::SEMICOLON)
next_token();
return stmt;
}
ast::let_stmt* parser::parse_let() {
ast::let_stmt* stmt = new ast::let_stmt(current);
if (!expect_next(token::type::IDENTIFIER)) {
delete stmt;
skip_until_semicolon();
return nullptr;
}
stmt->name = new ast::identifier{current, current.literal};
if (!expect_next(token::type::ASSIGN)) {
delete stmt;
skip_until_semicolon();
return nullptr;
}
next_token();
stmt->value = parse_expression();
if (next.type == token::type::SEMICOLON)
next_token();
return stmt;
}
ast::expression_stmt* parser::parse_expression_stmt() {
TRACE_FUNCTION;
ast::expression_stmt* stmt = new ast::expression_stmt(current);
stmt->expression = parse_expression();
if (next.type == token::type::SEMICOLON)
next_token();
return stmt;
};
bool parser::expect_next(token::type t) { bool parser::expect_next(token::type t) {
if (next.type == t) { if (next.type == t) {
@@ -49,27 +201,6 @@ namespace parser {
return false; return false;
} }
ast::let* parser::parse_let() {
ast::let* stmt = new ast::let(current);
if (!expect_next(token::type::IDENTIFIER)) {
delete stmt;
return nullptr;
}
stmt->name = new ast::identifier{current, current.literal};
if (!expect_next(token::type::ASSIGN)) {
delete stmt;
return nullptr;
}
// TODO: we are currently skipping expressions until we encounter a
// semicolon
for (; current.type != token::type::SEMICOLON; next_token()) {}
return stmt;
}
void parser::next_error(token::type t) { void parser::next_error(token::type t) {
std::stringstream ss; std::stringstream ss;
ss << "Expected next token to be " << t << " but instead got " ss << "Expected next token to be " << t << " but instead got "
@@ -77,8 +208,222 @@ namespace parser {
errors.push_back(new ast::error::expected_next(t, ss.str())); errors.push_back(new ast::error::expected_next(t, ss.str()));
} }
void parser::unkown_prefix_error(token::token tok) {
std::stringstream ss;
ss << "No prefix parse function for token " << tok;
errors.push_back(new ast::error::unkown_prefix(tok, ss.str()));
}
parser::~parser() { parser::~parser() {
for (const auto& e : errors) for (const auto& e : errors)
delete e; delete e;
} }
void parser::register_prefix(token::type type, prefix_parse_fn fn) {
prefix_parse_fns[type] = fn;
};
void parser::register_prefix(
std::vector<token::type> types, prefix_parse_fn fn
) {
for (auto& type : types)
register_prefix(type, fn);
};
void parser::register_infix(token::type type, infix_parse_fn fn) {
infix_parse_fns[type] = fn;
};
void
parser::register_infix(std::vector<token::type> types, infix_parse_fn fn) {
for (auto& type : types)
register_infix(type, fn);
};
ast::identifier* parser::parse_identifier() {
return new ast::identifier(current, current.literal);
};
ast::integer_literal* parser::parse_integer() {
TRACE_FUNCTION;
return new ast::integer_literal(current, std::stoi(current.literal));
};
ast::boolean_literal* parser::parse_boolean() {
TRACE_FUNCTION;
return new ast::boolean_literal(
current,
current.type == token::type::TRUE
);
};
static void free_vec(std::vector<ast::identifier*> v) {
for (auto& e : v)
delete e;
}
std::vector<ast::identifier*> parser::parse_function_parameters() {
if (next.type == token::type::RPAREN) {
next_token();
return {}; // no params
}
std::vector<ast::identifier*> ret;
if (!expect_next(token::type::IDENTIFIER))
return {};
ret.push_back(parse_identifier());
while (next.type == token::type::COMMA) {
next_token();
if (!expect_next(token::type::IDENTIFIER)) {
free_vec(ret);
return {};
}
ret.push_back(parse_identifier());
}
if (current.type == token::type::COMMA
&& next.type == token::type::RPAREN) {
next_error(token::type::IDENTIFIER);
free_vec(ret);
return {};
}
if (!expect_next(token::type::RPAREN)) {
free_vec(ret);
return {};
}
return ret;
}
ast::function_literal* parser::parse_function_lit() {
TRACE_FUNCTION;
ast::function_literal* ret = new ast::function_literal(current);
if (!expect_next(token::type::LPAREN)) {
delete ret;
return nullptr;
}
ret->parameters = parse_function_parameters();
if (!expect_next(token::type::LBRACE)) {
delete ret;
return nullptr;
}
ret->body = parse_block();
return ret;
};
ast::prefix_expr* parser::parse_prefix_expr() {
TRACE_FUNCTION;
ast::prefix_expr* ret = new ast::prefix_expr(current, current.literal);
next_token();
ret->right = parse_expression(precedence::PREFIX);
return ret;
};
ast::expression* parser::parse_grouped_expr() {
TRACE_FUNCTION;
next_token();
ast::expression* ret = parse_expression(precedence::LOWEST);
if (!expect_next(token::type::RPAREN)) {
delete ret;
return nullptr;
}
return ret;
};
ast::if_then_else* parser::parse_if_then_else() {
TRACE_FUNCTION;
ast::if_then_else* ret = new ast::if_then_else(current);
if (!expect_next(token::type::LPAREN)) {
delete ret;
return nullptr;
}
next_token();
ret->condition = parse_expression();
if (!expect_next(token::type::RPAREN)) {
delete ret;
return nullptr;
}
if (!expect_next(token::type::LBRACE)) {
delete ret;
return nullptr;
}
ret->consequence = parse_block();
if (next.type != token::type::ELSE)
return ret;
next_token();
if (!expect_next(token::type::LBRACE)) {
delete ret;
return nullptr;
}
ret->alternative = parse_block();
return ret;
};
ast::block_stmt* parser::parse_block() {
TRACE_FUNCTION;
ast::block_stmt* ret = new ast::block_stmt(current);
for (next_token(); current.type != token::type::RBRACE
&& current.type != token::type::END_OF_FILE;
next_token()) {
ast::statement* stmt = parse_statement();
if (stmt != nullptr)
ret->statements.push_back(stmt);
}
if (current.type != token::type::RBRACE) {
next_error(token::type::RBRACE);
delete ret;
return nullptr;
}
return ret;
}
ast::infix_expr* parser::parse_infix_expr(ast::expression* left) {
TRACE_FUNCTION;
ast::infix_expr* ret =
new ast::infix_expr(current, current.literal, left);
precedence prec = precedence_for(current.type);
next_token();
ret->right = parse_expression(prec);
return ret;
};
ast::function_call* parser::parse_function_call(ast::expression* target) {
TRACE_FUNCTION;
ast::function_call* ret = new ast::function_call(current, target);
next_token();
if (current.type == token::type::RPAREN)
return ret;
ret->parameters.push_back(parse_expression());
// parameters
while (next.type == token::type::COMMA) {
next_token();
next_token();
ret->parameters.push_back(parse_expression());
}
if (!expect_next(token::type::RPAREN)) {
delete ret;
return nullptr;
}
return ret;
};
} // namespace parser } // namespace parser

View File

@@ -2,26 +2,72 @@
#include "ast/ast.hpp" #include "ast/ast.hpp"
#include "ast/errors/error.hpp" #include "ast/errors/error.hpp"
#include "ast/expressions/boolean.hpp"
#include "ast/expressions/function.hpp"
#include "ast/expressions/identifier.hpp"
#include "ast/expressions/if_then_else.hpp"
#include "ast/expressions/infix.hpp"
#include "ast/expressions/integer.hpp"
#include "ast/expressions/prefix.hpp"
#include "ast/program.hpp"
#include "ast/statements/block.hpp"
#include "ast/statements/expression.hpp"
#include "ast/statements/let.hpp" #include "ast/statements/let.hpp"
#include "ast/statements/return.hpp"
#include "lexer/lexer.hpp" #include "lexer/lexer.hpp"
#include "precedence.hpp"
#include "token/token.hpp" #include "token/token.hpp"
#include <functional>
#include <memory>
#include <vector>
namespace parser { namespace parser {
using prefix_parse_fn = std::function<ast::expression*()>;
using infix_parse_fn = std::function<ast::expression*(ast::expression*)>;
struct parser { struct parser {
parser(lexer::lexer& lexer); parser(lexer::lexer&);
~parser(); ~parser();
std::vector<ast::error::error*> errors; std::vector<ast::error::error*> errors;
ast::program* parse_program(); std::unique_ptr<ast::program> parse_program();
private: private:
lexer::lexer& lexer; lexer::lexer& lexer;
token::token current, next; token::token current, next;
std::unordered_map<token::type, prefix_parse_fn> prefix_parse_fns;
std::unordered_map<token::type, infix_parse_fn> infix_parse_fns;
void next_token(); void next_token();
void skip_until_semicolon();
ast::statement* parse_statement(); ast::statement* parse_statement();
ast::let* parse_let(); ast::expression* parse_expression(precedence = precedence::LOWEST);
ast::let_stmt* parse_let();
ast::return_stmt* parse_return();
ast::expression_stmt* parse_expression_stmt();
bool expect_next(token::type); bool expect_next(token::type);
void next_error(token::type); void next_error(token::type);
void unkown_prefix_error(token::token);
void register_prefix(token::type, prefix_parse_fn);
void register_prefix(std::vector<token::type>, prefix_parse_fn);
void register_infix(token::type, infix_parse_fn);
void register_infix(std::vector<token::type>, infix_parse_fn);
ast::identifier* parse_identifier();
ast::integer_literal* parse_integer();
ast::boolean_literal* parse_boolean();
std::vector<ast::identifier*> parse_function_parameters();
ast::function_literal* parse_function_lit();
ast::prefix_expr* parse_prefix_expr();
ast::expression* parse_grouped_expr();
ast::if_then_else* parse_if_then_else();
ast::block_stmt* parse_block();
ast::infix_expr* parse_infix_expr(ast::expression*);
ast::function_call* parse_function_call(ast::expression*);
}; };
} // namespace parser } // namespace parser

24
src/parser/precedence.cpp Normal file
View File

@@ -0,0 +1,24 @@
#include "precedence.hpp"
namespace parser {
precedence precedence_for(token::type type) {
switch (type) {
case token::type::EQ:
case token::type::NEQ:
return precedence::EQUALS;
case token::type::LT:
case token::type::GT:
return precedence::LESS_GREATER;
case token::type::PLUS:
case token::type::MINUS:
return precedence::SUM;
case token::type::ASTERISK:
case token::type::SLASH:
return precedence::PRODUCT;
case token::type::LPAREN:
return precedence::CALL;
default:
return precedence::LOWEST;
}
}
} // namespace parser

21
src/parser/precedence.hpp Normal file
View File

@@ -0,0 +1,21 @@
#include "token/type.hpp"
#include <ostream>
namespace parser {
enum class precedence {
LOWEST,
EQUALS,
LESS_GREATER,
SUM,
PRODUCT,
PREFIX,
CALL
};
precedence precedence_for(token::type);
inline std::ostream& operator<<(std::ostream& os, precedence& p) {
return os << static_cast<int>(p);
}
} // namespace parser

View File

@@ -1,7 +1,10 @@
#include "repl.hpp" #include "repl.hpp"
#include "ast/program.hpp"
#include "lexer/lexer.hpp" #include "lexer/lexer.hpp"
#include "parser/parser.hpp"
#include <memory>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -19,11 +22,14 @@ namespace repl {
std::istringstream ss(line); std::istringstream ss(line);
lexer::lexer l{ss}; lexer::lexer l{ss};
for (token::token tok = l.next_token(); parser::parser p{l};
tok.type != token::type::END_OF_FILE; std::unique_ptr<ast::program> program = p.parse_program();
tok = l.next_token()) if (!p.errors.empty()) {
out << tok << " "; for (auto& e : p.errors)
out << std::endl; out << e->what() << std::endl;
continue;
}
out << program->str() << std::endl;
} }
} }

30
src/utils/tracer.hpp Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include <iostream>
#include <string>
namespace {
struct FunctionTracer {
std::string name;
inline static int tab_counter = 0;
FunctionTracer(const std::string& func): name(func) {
std::cout << std::string(tab_counter++, '\t') << "BEGIN " << name
<< std::endl;
}
~FunctionTracer() {
std::cout << std::string(--tab_counter, '\t') << "Exiting " << name
<< std::endl;
}
};
} // namespace
#ifndef TRACE
#define TRACE 0
#endif
#if TRACE
#define TRACE_FUNCTION FunctionTracer tracer(__FUNCTION__);
#else
#define TRACE_FUNCTION
#endif

View File

@@ -0,0 +1,20 @@
#include "doctest.h"
#include "utils.hpp"
TEST_SUITE("Eval: integers") {
TEST_CASE_FIXTURE(
test::utils::EvalFixture,
"Simple integer expression statements"
) {
struct test_struct {
std::string input;
int expected;
};
struct test_struct tests[]{{"5", 5}, {"10", 10}};
for (auto& t : tests) {
setup(t.input);
test::utils::test_integer_object(result.get(), t.expected);
}
}
}

View File

@@ -6,6 +6,7 @@
#include <sstream> #include <sstream>
#include <string> #include <string>
TEST_SUITE("Lexer") {
TEST_CASE("Single character token") { TEST_CASE("Single character token") {
struct test { struct test {
token::type expectedType; token::type expectedType;
@@ -159,3 +160,4 @@ if (5 < 10) {\
REQUIRE(tok.literal == t.expectedLiteral); REQUIRE(tok.literal == t.expectedLiteral);
} }
}; };
}

View File

@@ -1,134 +0,0 @@
#include "parser/parser.hpp"
#include "ast/ast.hpp"
#include "ast/statements/let.hpp"
#include "lexer/lexer.hpp"
#include <doctest.h>
#include <iostream>
#include <sstream>
void test_let_statement(ast::statement* stmt, const std::string name) {
REQUIRE(stmt->token_literal() == "let");
ast::let* let_stmt;
REQUIRE_NOTHROW(let_stmt = dynamic_cast<ast::let*>(stmt));
REQUIRE_MESSAGE(
let_stmt != nullptr,
"Couldn't cast statement to a let statement"
);
REQUIRE(let_stmt->name->value == name);
REQUIRE(let_stmt->name->token_literal() == name);
}
void test_failing_let_parsing(
std::string input_s,
std::vector<token::type> expected_types,
int n_good_statements = 0
) {
std::stringstream input(input_s);
lexer::lexer l{input};
parser::parser p{l};
ast::program* program = p.parse_program();
// Check for errors
REQUIRE(p.errors.size() == expected_types.size());
int i = 0;
for (auto& e : p.errors) {
ast::error::expected_next* en;
REQUIRE_NOTHROW(en = dynamic_cast<ast::error::expected_next*>(e));
REQUIRE_MESSAGE(
en != nullptr,
"Couldn't cast the error to an 'expected_next'"
);
REQUIRE(en->expected_type == expected_types[i++]);
}
// normal program check
REQUIRE_MESSAGE(
program != nullptr,
"parse_program() returned a null pointer"
);
REQUIRE(program->statements.size() == n_good_statements);
delete program;
}
void check_parser_errors(const std::vector<ast::error::error*>& errors) {
if (errors.empty())
return;
std::cerr << "parser has " << errors.size() << " errors:\n";
for (const auto& error : errors)
std::cerr << '\t' << error->what() << "\n";
// Use doctest's FAIL macro to immediately stop
FAIL_CHECK("Parser had errors. See stderr for details.");
}
TEST_CASE("Malformed let statement (checking for memory leaks)") {
SUBCASE("Second token not identifier") {
test_failing_let_parsing("let 5 = 5;", {token::type::IDENTIFIER});
}
SUBCASE("Third token not '='") {
test_failing_let_parsing("let five ! 5;", {token::type::ASSIGN});
}
SUBCASE("Missing both identifier and '='") {
test_failing_let_parsing("let 5;", {token::type::IDENTIFIER});
}
SUBCASE("Multiple parsing errors") {
test_failing_let_parsing(
"let 5; let ! = 5; let five = 5; let five 5; let;",
{token::type::IDENTIFIER,
token::type::IDENTIFIER,
token::type::ASSIGN,
token::type::IDENTIFIER},
1
);
}
}
TEST_CASE("Parse let statement") {
std::stringstream input("\
let x = 5;\
let y = 10;\
let foobar = 103213;\
");
lexer::lexer l{input};
parser::parser p{l};
ast::program* program = p.parse_program();
check_parser_errors(p.errors);
REQUIRE_MESSAGE(
program != nullptr,
"parse_program() returned a null pointer"
);
REQUIRE(program->statements.size() == 3);
struct test {
std::string expected_identifier;
};
test tests[]{
"x",
"y",
"foobar",
};
int i = 0;
for (const auto& t : tests) {
ast::statement* stmt = program->statements[i++];
test_let_statement(stmt, t.expected_identifier);
}
delete program;
}

278
test/parser/expression.cpp Normal file
View File

@@ -0,0 +1,278 @@
#include "ast/expressions/identifier.hpp"
#include "ast/expressions/infix.hpp"
#include "ast/expressions/prefix.hpp"
#include "utils.hpp"
#include <doctest.h>
TEST_SUITE("Parser: expression") {
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Simple expression statement with identifier"
) {
setup("foobar;");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expression_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::identifier* ident =
test::utils::cast<ast::identifier>(expression_stmt->expression);
REQUIRE(ident->value == "foobar");
REQUIRE(ident->token_literal() == "foobar");
};
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Simple expression statement with integer"
) {
setup("5;");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expression_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
test::utils::test_integer_literal(expression_stmt->expression, 5);
};
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Simple statements with booleans"
) {
SUBCASE("True literal") {
setup("true;");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expression_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
test::utils::test_boolean_literal(
expression_stmt->expression,
true
);
}
SUBCASE("False literal") {
setup("false;");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expression_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
test::utils::test_boolean_literal(
expression_stmt->expression,
false
);
}
SUBCASE("True let statement") {
setup("let foo = true;");
REQUIRE(program->statements.size() == 1);
ast::let_stmt* let_stmt =
test::utils::cast<ast::let_stmt>(program->statements[0]);
CHECK(let_stmt->name->value == "foo");
test::utils::test_boolean_literal(let_stmt->value, true);
}
SUBCASE("False let statement") {
setup("let bar = false;");
REQUIRE(program->statements.size() == 1);
ast::let_stmt* let_stmt =
test::utils::cast<ast::let_stmt>(program->statements[0]);
CHECK(let_stmt->name->value == "bar");
test::utils::test_boolean_literal(let_stmt->value, false);
}
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Simple expression statement with prefix before integer"
) {
#define CASE(name, input, _op, _right) \
SUBCASE(name) { \
setup(input); \
\
REQUIRE(program->statements.size() == 1); \
ast::expression_stmt* expression_stmt = \
test::utils::cast<ast::expression_stmt>(program->statements[0]); \
\
ast::prefix_expr* prefix_expr = \
test::utils::cast<ast::prefix_expr>(expression_stmt->expression); \
\
REQUIRE(prefix_expr->op == _op); \
test::utils::test_integer_literal(prefix_expr->right, _right); \
}
CASE("Prefix: '!'", "!5;", "!", 5);
CASE("Prefix: '-'", "-15;", "-", 15);
#undef CASE
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Simple expression statement with infix between integers"
) {
#define CASE(name, input, _left, _op, _right) \
SUBCASE(name) { \
setup(input); \
REQUIRE(program->statements.size() == 1); \
\
ast::expression_stmt* expression_stmt = \
test::utils::cast<ast::expression_stmt>(program->statements[0]); \
\
test::utils::test_infix_expression( \
expression_stmt->expression, \
_left, \
_op, \
_right \
); \
}
CASE("Infix: '+'", "5 + 5;", 5, "+", 5);
CASE("Infix: '-'", "5- 5;", 5, "-", 5);
CASE("Infix: '*'", "15 *5;", 15, "*", 5);
CASE("Infix: '/'", "15 / 5;", 15, "/", 5);
CASE("Infix: '<'", "15 < 15;", 15, "<", 15);
CASE("Infix: '>'", "25 > 15;", 25, ">", 15);
CASE("Infix: '=='", "5 == 5;", 5, "==", 5);
CASE("Infix: '!='", "15 != 5;", 15, "!=", 5);
CASE("Infix: between identifiers", "alice * bob;", "alice", "*", "bob");
CASE("Infix: between booleans", "true == bob;", true, "==", "bob");
CASE("Infix: between booleans", "true != false;", true, "!=", false);
#undef CASE
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Slightly more complex infix expression"
) {
setup("5 - -15;");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expression_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::infix_expr* infix_expr =
test::utils::cast<ast::infix_expr>(expression_stmt->expression);
test::utils::test_integer_literal(infix_expr->left, 5);
CHECK(infix_expr->op == "-");
ast::prefix_expr* prefix_expr =
test::utils::cast<ast::prefix_expr>(infix_expr->right);
CHECK(prefix_expr->op == "-");
test::utils::test_integer_literal(prefix_expr->right, 15);
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Slightly even more complex infix expression"
) {
setup("5 - -15 * 3;");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expression_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::infix_expr* infix_expr =
test::utils::cast<ast::infix_expr>(expression_stmt->expression);
test::utils::test_integer_literal(infix_expr->left, 5);
CHECK(infix_expr->op == "-");
ast::infix_expr* second_infix_expr =
test::utils::cast<ast::infix_expr>(infix_expr->right);
CHECK(second_infix_expr->op == "*");
ast::prefix_expr* prefix_expr =
test::utils::cast<ast::prefix_expr>(second_infix_expr->left);
CHECK(prefix_expr->op == "-");
test::utils::test_integer_literal(prefix_expr->right, 15);
test::utils::test_integer_literal(second_infix_expr->right, 3);
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Checking operator precedence with simple example"
) {
setup("5 * -15 - 3;");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expression_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::infix_expr* infix_expr =
test::utils::cast<ast::infix_expr>(expression_stmt->expression);
ast::infix_expr* second_infix_expr =
test::utils::cast<ast::infix_expr>(infix_expr->left);
CHECK(second_infix_expr->op == "*");
test::utils::test_integer_literal(second_infix_expr->left, 5);
ast::prefix_expr* prefix_expr =
test::utils::cast<ast::prefix_expr>(second_infix_expr->right);
CHECK(prefix_expr->op == "-");
test::utils::test_integer_literal(prefix_expr->right, 15);
CHECK(infix_expr->op == "-");
test::utils::test_integer_literal(infix_expr->right, 3);
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Full Operator Precedence test"
) {
struct test {
std::string input;
std::string expected;
};
struct test tests[]{
{"-a * b", "((-a) * b)"},
{"!-a", "(!(-a))"},
{"a + b + c", "((a + b) + c)"},
{"a + b - c", "((a + b) - c)"},
{"a * b * c", "((a * b) * c)"},
{"a * b / c", "((a * b) / c)"},
{"a + b / c", "(a + (b / c))"},
{"a + b * c + d / e - f", "(((a + (b * c)) + (d / e)) - f)"},
{"3 + 4; -5 * 5", "(3 + 4)((-5) * 5)"},
{"5 > 4 == 3 < 4", "((5 > 4) == (3 < 4))"},
{"5 < 4 != 3 > 4", "((5 < 4) != (3 > 4))"},
{"3 + 4 * 5 == 3 * 1 + 4 * 5",
"((3 + (4 * 5)) == ((3 * 1) + (4 * 5)))"},
{"3 > 5 == false", "((3 > 5) == false)"},
{"3 < 5 == true", "((3 < 5) == true)"},
{"1 + (2 + 3)", "(1 + (2 + 3))"},
{"(1 + a) * 17", "((1 + a) * 17)"},
{"2 / (5 + 5)", "(2 / (5 + 5))"},
{"-(5 + a)", "(-(5 + a))"},
{"!(true == true)", "(!(true == true))"},
{"a + add(b * c) + d", "((a + add((b * c))) + d)"},
{"add(a, b, 1, 2 * 3, 4 + 5, add(6, 7 * 8))",
"add(a, b, 1, (2 * 3), (4 + 5), add(6, (7 * 8)))"},
{"add(a + b + c * d / f + g)",
"add((((a + b) + ((c * d) / f)) + g))"},
};
for (auto& t : tests) {
setup(t.input);
CHECK(program->str() == t.expected);
}
}
TEST_CASE_FIXTURE(test::utils::ParserFixture, "Test to see trace") {
setup("-1 * 2 - 3");
CHECK(program->str() == "(((-1) * 2) - 3)");
setup("-1 - 2 * 3");
CHECK(program->str() == "((-1) - (2 * 3))");
}
}

372
test/parser/function.cpp Normal file
View File

@@ -0,0 +1,372 @@
#include "ast/expressions/function.hpp"
#include "ast/errors/error.hpp"
#include "ast/statements/return.hpp"
#include "token/type.hpp"
#include "utils.hpp"
#include <doctest.h>
TEST_SUITE("Parser: function literal") {
TEST_CASE("Malformed function literal (checking for memory leaks)") {
SUBCASE("Missing opening paren no param") {
test::utils::test_failing_parsing(
"fn ) { return 2; }",
{token::type::LPAREN}
);
}
SUBCASE("Missing opening paren one param") {
test::utils::test_failing_parsing(
"fn x) { return 2; }",
{token::type::LPAREN}
);
}
SUBCASE("Missing opening paren two param") {
test::utils::test_failing_parsing(
"fn x, y) { return 2; }",
{token::type::LPAREN}
);
}
SUBCASE("Missing identifier with no closing paren - no param") {
test::utils::test_failing_parsing(
"fn ( { return 2; }",
{token::type::IDENTIFIER}
);
}
SUBCASE("Missing identifier with no closing paren - one param") {
test::utils::test_failing_parsing(
"fn (x, { return 2; }",
{token::type::IDENTIFIER}
);
}
SUBCASE("Missing identifier with no closing paren - two params") {
test::utils::test_failing_parsing(
"fn (x, y, { return 2; }",
{token::type::IDENTIFIER}
);
}
SUBCASE("Missing identifier with closing paren - one param") {
test::utils::test_failing_parsing(
"fn (x,) { return 2; }",
{token::type::IDENTIFIER}
);
}
SUBCASE("Missing identifier with closing paren - two params") {
test::utils::test_failing_parsing(
"fn (x,y,) { return 2; }",
{token::type::IDENTIFIER}
);
}
SUBCASE("Missing closing paren one param") {
test::utils::test_failing_parsing(
"fn (x { return 2; }",
{token::type::RPAREN}
);
}
SUBCASE("Missing closing paren two params") {
test::utils::test_failing_parsing(
"fn (x, y { return 2; }",
{token::type::RPAREN}
);
}
SUBCASE("Missing opening brace") {
test::utils::test_failing_parsing(
"fn (x, y) return x + y; }",
{token::type::LBRACE}
);
}
SUBCASE("Missing closing brace") {
test::utils::test_failing_parsing(
"fn (x, y) { return x + y; ",
{token::type::RBRACE}
);
}
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Parse well formed function literal"
) {
SUBCASE("no param function") {
setup("\
fn () {\
return true;\
}\
");
REQUIRE(program->statements.size() == 1);
CHECK(program->statements[0]->token_literal() == "fn");
ast::expression_stmt* expr_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::function_literal* fun =
test::utils::cast<ast::function_literal>(expr_stmt->expression);
// parameters
CHECK(fun->parameters.size() == 0);
// block
REQUIRE(fun->body->statements.size() == 1);
ast::return_stmt* ret =
test::utils::cast<ast::return_stmt>(fun->body->statements[0]);
test::utils::test_boolean_literal(ret->value, true);
// full string
CHECK(fun->str() == "fn (){return true;}");
}
SUBCASE("one param function") {
setup("\
fn (x) {\
return x + 1;\
}\
");
REQUIRE(program->statements.size() == 1);
CHECK(program->statements[0]->token_literal() == "fn");
ast::expression_stmt* expr_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::function_literal* fun =
test::utils::cast<ast::function_literal>(expr_stmt->expression);
// parameters
REQUIRE(fun->parameters.size() == 1);
test::utils::test_identifier(fun->parameters[0], "x");
// block
REQUIRE(fun->body->statements.size() == 1);
ast::return_stmt* ret =
test::utils::cast<ast::return_stmt>(fun->body->statements[0]);
test::utils::test_infix_expression(ret->value, "x", "+", 1);
// full string
CHECK(fun->str() == "fn (x){return (x + 1);}");
}
SUBCASE("two param function") {
setup("\
fn (x, y) {\
return x + y;\
}\
");
REQUIRE(program->statements.size() == 1);
CHECK(program->statements[0]->token_literal() == "fn");
ast::expression_stmt* expr_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::function_literal* fun =
test::utils::cast<ast::function_literal>(expr_stmt->expression);
// parameters
REQUIRE(fun->parameters.size() == 2);
test::utils::test_identifier(fun->parameters[0], "x");
test::utils::test_identifier(fun->parameters[1], "y");
// block
REQUIRE(fun->body->statements.size() == 1);
ast::return_stmt* ret =
test::utils::cast<ast::return_stmt>(fun->body->statements[0]);
test::utils::test_infix_expression(ret->value, "x", "+", "y");
// full string
CHECK(fun->str() == "fn (x, y){return (x + y);}");
}
SUBCASE("two param function assigned to variable") {
setup("\
let fun = fn (x, y) {\
return x + y;\
}\
");
REQUIRE(program->statements.size() == 1);
CHECK(program->statements[0]->token_literal() == "let");
ast::let_stmt* let_stmt =
test::utils::cast<ast::let_stmt>(program->statements[0]);
// let lhs
test::utils::test_identifier(let_stmt->name, "fun");
// let rhs
CHECK(let_stmt->value->token_literal() == "fn");
ast::function_literal* fun =
test::utils::cast<ast::function_literal>(let_stmt->value);
// parameters
REQUIRE(fun->parameters.size() == 2);
test::utils::test_identifier(fun->parameters[0], "x");
test::utils::test_identifier(fun->parameters[1], "y");
// block
REQUIRE(fun->body->statements.size() == 1);
ast::return_stmt* ret =
test::utils::cast<ast::return_stmt>(fun->body->statements[0]);
test::utils::test_infix_expression(ret->value, "x", "+", "y");
// full string
CHECK(fun->str() == "fn (x, y){return (x + y);}");
}
}
}
TEST_SUITE("Parser: function call") {
TEST_CASE("Malformed function call (checking for memory leaks)") {
SUBCASE("missing closing no param function") {
std::stringstream input("value(;");
lexer::lexer l{input};
parser::parser p{l};
std::unique_ptr<ast::program> program = p.parse_program();
INFO(*program);
// Check for errors
REQUIRE(p.errors.size() == 2);
ast::error::unkown_prefix* up =
test::utils::cast<ast::error::unkown_prefix>(p.errors[0]);
REQUIRE(up->prefix.type == token::type::SEMICOLON);
ast::error::expected_next* en =
test::utils::cast<ast::error::expected_next>(p.errors[1]);
REQUIRE(en->expected_type == token::type::RPAREN);
// normal program check
REQUIRE_MESSAGE(
program != nullptr,
"parse_program() returned a null pointer"
);
}
SUBCASE("missing closing 1 param function") {
test::utils::test_failing_parsing(
"value(x;",
{token::type::RPAREN}
);
}
SUBCASE("missing closing 2 param function") {
test::utils::test_failing_parsing(
"value(x, y;",
{token::type::RPAREN}
);
}
SUBCASE("missing comma between 2 param function") {
test::utils::test_failing_parsing(
"value(x y);",
{token::type::RPAREN}
);
}
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Parse well formed function call"
) {
SUBCASE("no param function") {
setup("value();");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expr_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::function_call* fun =
test::utils::cast<ast::function_call>(expr_stmt->expression);
// target
test::utils::test_identifier(fun->target, "value");
// parameters
CHECK(fun->parameters.size() == 0);
// full string
CHECK(fun->str() == "value()");
}
SUBCASE("one param identifier function") {
setup("is_odd(1);");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expr_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::function_call* fun =
test::utils::cast<ast::function_call>(expr_stmt->expression);
// target
test::utils::test_identifier(fun->target, "is_odd");
// parameters
CHECK(fun->parameters.size() == 1);
test::utils::test_integer_literal(fun->parameters[0], 1);
// full string
CHECK(fun->str() == "is_odd(1)");
}
SUBCASE("two param function") {
setup("is_gt(1, a + 10);");
REQUIRE(program->statements.size() == 1);
ast::expression_stmt* expr_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::function_call* fun =
test::utils::cast<ast::function_call>(expr_stmt->expression);
// target
test::utils::test_identifier(fun->target, "is_gt");
// parameters
CHECK(fun->parameters.size() == 2);
test::utils::test_integer_literal(fun->parameters[0], 1);
test::utils::test_infix_expression(
fun->parameters[1],
"a",
"+",
10
);
// full string
CHECK(fun->str() == "is_gt(1, (a + 10))");
}
SUBCASE("two param identifier function assigned to variable") {
setup("let res = add(1, a + 10, 2 * 3);");
REQUIRE(program->statements.size() == 1);
ast::let_stmt* let_stmt =
test::utils::cast<ast::let_stmt>(program->statements[0]);
test::utils::test_identifier(let_stmt->name, "res");
ast::function_call* fun =
test::utils::cast<ast::function_call>(let_stmt->value);
// target
test::utils::test_identifier(fun->target, "add");
// parameters
CHECK(fun->parameters.size() == 3);
test::utils::test_integer_literal(fun->parameters[0], 1);
test::utils::test_infix_expression(
fun->parameters[1],
"a",
"+",
10
);
test::utils::test_infix_expression(fun->parameters[2], 2, "*", 3);
// full string
CHECK(fun->str() == "add(1, (a + 10), (2 * 3))");
CHECK(let_stmt->str() == "let res = add(1, (a + 10), (2 * 3));");
}
}
}

142
test/parser/if.cpp Normal file
View File

@@ -0,0 +1,142 @@
#include "ast/expressions/if_then_else.hpp"
#include "ast/expressions/infix.hpp"
#include "ast/statements/block.hpp"
#include "ast/statements/expression.hpp"
#include "utils.hpp"
#include <doctest.h>
TEST_SUITE("Parser: if") {
TEST_CASE("Malformed if then else (checking for memory leaks)") {
SUBCASE("Missing opening paren") {
test::utils::test_failing_parsing(
"if x > 15) {\
return x;\
}",
{token::type::LPAREN}
);
}
SUBCASE("Missing closing paren") {
test::utils::test_failing_parsing(
"if (x > 15 {\
return x;\
}",
{token::type::RPAREN}
);
}
SUBCASE("Missing opening brace") {
test::utils::test_failing_parsing(
"if (x > 15) \
return x;\
}",
{token::type::LBRACE}
);
}
SUBCASE("Missing closing brace") {
test::utils::test_failing_parsing(
"if (x > 15) { \
return x;\
",
{token::type::RBRACE}
);
}
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Parse well formed simple if "
) {
setup("\
if (x > 15) {\
return x;\
}\
");
REQUIRE(program->statements.size() == 1);
CHECK(program->statements[0]->token_literal() == "if");
ast::expression_stmt* expr_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::if_then_else* if_stmt =
test::utils::cast<ast::if_then_else>(expr_stmt->expression);
// condition
ast::infix_expr* cond =
test::utils::cast<ast::infix_expr>(if_stmt->condition);
test::utils::test_infix_expression(cond, "x", ">", 15);
CHECK(if_stmt->condition->str() == "(x > 15)");
// consequence
REQUIRE(if_stmt->consequence->statements.size() == 1);
ast::return_stmt* ret_stmt = test::utils::cast<ast::return_stmt>(
if_stmt->consequence->statements[0]
);
test::utils::test_identifier(ret_stmt->value, "x");
CHECK(if_stmt->consequence->str() == "{return x;}");
// alternative
CHECK(if_stmt->alternative == nullptr);
// full string
CHECK(if_stmt->str() == "if (x > 15){return x;}");
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Parse well formed if then else"
) {
setup("\
if (x < 5) {\
return 15;\
} else {\
let b = 1;\
return x;\
}\
");
REQUIRE(program->statements.size() == 1);
CHECK(program->statements[0]->token_literal() == "if");
ast::expression_stmt* expr_stmt =
test::utils::cast<ast::expression_stmt>(program->statements[0]);
ast::if_then_else* if_stmt =
test::utils::cast<ast::if_then_else>(expr_stmt->expression);
// condition
ast::infix_expr* cond =
test::utils::cast<ast::infix_expr>(if_stmt->condition);
test::utils::test_infix_expression(cond, "x", "<", 5);
CHECK(if_stmt->condition->str() == "(x < 5)");
// consequence
REQUIRE(if_stmt->consequence->statements.size() == 1);
ast::return_stmt* ret_stmt = test::utils::cast<ast::return_stmt>(
if_stmt->consequence->statements[0]
);
test::utils::test_integer_literal(ret_stmt->value, 15);
CHECK(if_stmt->consequence->str() == "{return 15;}");
// alternative
REQUIRE(if_stmt->alternative != nullptr);
REQUIRE(if_stmt->alternative->statements.size() == 2);
ast::let_stmt* let_stmt = test::utils::cast<ast::let_stmt>(
if_stmt->alternative->statements[0]
);
test::utils::test_identifier(let_stmt->name, "b");
test::utils::test_integer_literal(let_stmt->value, 1);
ast::return_stmt* ret_stmt2 = test::utils::cast<ast::return_stmt>(
if_stmt->alternative->statements[1]
);
test::utils::test_identifier(ret_stmt2->value, "x");
CHECK(if_stmt->alternative->str() == "{let b = 1;return x;}");
// full string
CHECK(
if_stmt->str() == "if (x < 5){return 15;}else{let b = 1;return x;}"
);
}
}

81
test/parser/let.cpp Normal file
View File

@@ -0,0 +1,81 @@
#include "ast/statements/let.hpp"
#include "ast/ast.hpp"
#include "utils.hpp"
#include <doctest.h>
#include <memory>
void test_let_statement(ast::statement* stmt, const std::string name) {
REQUIRE(stmt->token_literal() == "let");
ast::let_stmt* let_stmt = test::utils::cast<ast::let_stmt>(stmt);
REQUIRE(let_stmt->name->value == name);
REQUIRE(let_stmt->name->token_literal() == name);
}
TEST_SUITE("Parser: let") {
TEST_CASE("Malformed let statement (checking for memory leaks)") {
SUBCASE("Second token not identifier") {
test::utils::test_failing_parsing(
"let 5 = 5;",
{token::type::IDENTIFIER}
);
}
SUBCASE("Third token not '='") {
test::utils::test_failing_parsing(
"let five ! 5;",
{token::type::ASSIGN}
);
}
SUBCASE("Missing both identifier and '='") {
test::utils::test_failing_parsing(
"let 5;",
{token::type::IDENTIFIER}
);
}
SUBCASE("Multiple parsing errors") {
test::utils::test_failing_parsing(
"let 5; let ! = 5; let five = 5; let five 5; let;",
{token::type::IDENTIFIER,
token::type::IDENTIFIER,
token::type::ASSIGN,
token::type::IDENTIFIER},
1
);
}
}
TEST_CASE_FIXTURE(
test::utils::ParserFixture,
"Parse well formed let statements"
) {
setup("\
let x = 5;\
let y = 10;\
let foobar = 103213;\
");
REQUIRE(program->statements.size() == 3);
struct test {
std::string expected_identifier;
};
test tests[]{
"x",
"y",
"foobar",
};
int i = 0;
for (const auto& t : tests) {
ast::statement* stmt = program->statements[i++];
test_let_statement(stmt, t.expected_identifier);
}
}
}

View File

@@ -0,0 +1,51 @@
#include "parser/parser.hpp"
#include <doctest.h>
TEST_SUITE("Precedence") {
TEST_CASE("Raw precedence") {
CHECK(parser::precedence::LOWEST < parser::precedence::EQUALS);
CHECK(parser::precedence::LOWEST < parser::precedence::LESS_GREATER);
CHECK(parser::precedence::LOWEST < parser::precedence::SUM);
CHECK(parser::precedence::LOWEST < parser::precedence::PRODUCT);
CHECK(parser::precedence::LOWEST < parser::precedence::PREFIX);
CHECK(parser::precedence::LOWEST < parser::precedence::CALL);
CHECK(parser::precedence::EQUALS < parser::precedence::LESS_GREATER);
CHECK(parser::precedence::EQUALS < parser::precedence::SUM);
CHECK(parser::precedence::EQUALS < parser::precedence::PRODUCT);
CHECK(parser::precedence::EQUALS < parser::precedence::PREFIX);
CHECK(parser::precedence::EQUALS < parser::precedence::CALL);
CHECK(parser::precedence::LESS_GREATER < parser::precedence::SUM);
CHECK(parser::precedence::LESS_GREATER < parser::precedence::PRODUCT);
CHECK(parser::precedence::LESS_GREATER < parser::precedence::PREFIX);
CHECK(parser::precedence::LESS_GREATER < parser::precedence::CALL);
CHECK(parser::precedence::SUM < parser::precedence::PRODUCT);
CHECK(parser::precedence::SUM < parser::precedence::PREFIX);
CHECK(parser::precedence::SUM < parser::precedence::CALL);
CHECK(parser::precedence::PRODUCT < parser::precedence::PREFIX);
CHECK(parser::precedence::PRODUCT < parser::precedence::CALL);
CHECK(parser::precedence::PREFIX < parser::precedence::CALL);
}
TEST_CASE("Operator precedence") {
auto& prec = parser::precedence_for;
CHECK(prec(token::type::EQ) == prec(token::type::NEQ));
CHECK(prec(token::type::LT) == prec(token::type::GT));
CHECK(prec(token::type::PLUS) == prec(token::type::MINUS));
CHECK(prec(token::type::ASTERISK) == prec(token::type::SLASH));
CHECK(prec(token::type::EQ) < prec(token::type::LT));
CHECK(prec(token::type::EQ) < prec(token::type::PLUS));
CHECK(prec(token::type::EQ) < prec(token::type::ASTERISK));
CHECK(prec(token::type::LT) < prec(token::type::PLUS));
CHECK(prec(token::type::LT) < prec(token::type::ASTERISK));
CHECK(prec(token::type::PLUS) < prec(token::type::ASTERISK));
}
}

21
test/parser/return.cpp Normal file
View File

@@ -0,0 +1,21 @@
#include "utils.hpp"
#include <doctest.h>
TEST_SUITE("Parser: return") {
TEST_CASE_FIXTURE(test::utils::ParserFixture, "Parse return statement") {
setup("\
return 5;\
return 10;\
return 103213;\
return 12 + 34;\
");
REQUIRE(program->statements.size() == 4);
for (const auto stmt : program->statements) {
CHECK(stmt->token_literal() == "return");
test::utils::cast<ast::return_stmt>(stmt);
}
}
}

37
test/program.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include "ast/program.hpp"
#include "ast/statements/let.hpp"
#include "ast/statements/return.hpp"
#include "token/type.hpp"
#include <doctest.h>
TEST_SUITE("Program") {
TEST_CASE("Let string") {
ast::program program({new ast::let_stmt(
token::token(token::type::LET, "let"),
new ast::identifier(
token::token(token::type::IDENTIFIER, "myVar"),
"myVar"
),
new ast::identifier(
token::token(token::type::IDENTIFIER, "anotherVar"),
"anotherVar"
)
)});
CHECK(program.str() == "let myVar = anotherVar;");
}
TEST_CASE("Return string") {
ast::program program({new ast::return_stmt(
token::token(token::type::RETURN, "return"),
new ast::identifier(
token::token(token::type::IDENTIFIER, "myVar"),
"myVar"
)
)});
CHECK(program.str() == "return myVar;");
}
}

40
test/utils/fixtures.cpp Normal file
View File

@@ -0,0 +1,40 @@
#include "evaluator/evaluator.hpp"
#include "object/object.hpp"
#include "utils.hpp"
#include <doctest.h>
namespace test::utils {
void check_parser_errors(const std::vector<ast::error::error*>& errors) {
if (errors.empty())
return;
INFO("parser has " << errors.size() << " errors:");
std::ostringstream combined;
for (auto& err : errors)
combined << " > " << err->what() << '\n';
INFO(combined.str());
FAIL("Parser had errors.");
}
void ParserFixture::setup(std::string source) {
input.clear();
input << source;
lexer = std::make_unique<lexer::lexer>(input);
parser = std::make_unique<parser::parser>(*lexer);
program = parser->parse_program();
check_parser_errors(parser->errors);
REQUIRE_MESSAGE(
program != nullptr,
"parse_program() returned a null pointer"
);
}
void EvalFixture::setup(std::string source) {
ParserFixture::setup(source);
result = std::unique_ptr<object::object>(eval::eval(program.get()));
}
} // namespace test::utils

10
test/utils/test_eval.cpp Normal file
View File

@@ -0,0 +1,10 @@
#include "object/integers.hpp"
#include "object/object.hpp"
#include "utils.hpp"
namespace test::utils {
void test_integer_object(object::object* obj, int expected) {
object::integer* i = cast<object::integer>(obj);
CHECK(i->value == expected);
}
} // namespace test::utils

View File

@@ -0,0 +1,94 @@
#include "utils.hpp"
namespace test::utils {
void test_identifier(ast::expression* expr, std::string value) {
ast::identifier* ident = cast<ast::identifier>(expr);
REQUIRE(ident->value == value);
REQUIRE(ident->token_literal() == value);
}
void test_integer_literal(ast::expression* expr, int value) {
ast::integer_literal* int_lit = cast<ast::integer_literal>(expr);
REQUIRE(int_lit->value == value);
std::ostringstream oss;
oss << value;
REQUIRE(int_lit->token_literal() == oss.str());
}
void test_boolean_literal(ast::expression* expr, bool value) {
ast::boolean_literal* bool_lit = cast<ast::boolean_literal>(expr);
REQUIRE(bool_lit->value == value);
std::ostringstream oss;
oss << (value ? "true" : "false");
REQUIRE(bool_lit->token_literal() == oss.str());
}
void test_literal_expression(ast::expression* exp, std::any& expected) {
if (expected.type() == typeid(int))
return test_integer_literal(exp, std::any_cast<int>(expected));
if (expected.type() == typeid(bool))
return test_boolean_literal(exp, std::any_cast<bool>(expected));
if (expected.type() == typeid(std::string))
return test_identifier(exp, std::any_cast<std::string>(expected));
if (expected.type() == typeid(const char*))
return test_identifier(exp, std::any_cast<const char*>(expected));
FAIL(
"Type of exp not handled. Got: " * demangle(typeid(*exp).name())
* " as expression and " * demangle(expected.type().name())
* " as expected"
);
}
void test_infix_expression(
ast::expression* exp, std::any left, std::string op, std::any right
) {
ast::infix_expr* op_exp = cast<ast::infix_expr>(exp);
test_literal_expression(op_exp->left, left);
CHECK(op_exp->op == op);
test_literal_expression(op_exp->right, right);
}
void test_failing_parsing(
std::string input_s,
std::vector<token::type> expected_types,
int n_good_statements
) {
std::stringstream input(input_s);
lexer::lexer l{input};
parser::parser p{l};
std::unique_ptr<ast::program> program = p.parse_program();
INFO(*program);
// Check for errors
REQUIRE(p.errors.size() >= expected_types.size());
// ^^ because even though you were thinking
// about a specific token to be there, other `expect_next -> false`
// might be triggered for subexpressions due to the first one
int i = 0;
for (auto& t : expected_types) {
ast::error::expected_next* en =
cast<ast::error::expected_next>(p.errors[i++]);
REQUIRE(en->expected_type == t);
}
// normal program check
REQUIRE_MESSAGE(
program != nullptr,
"parse_program() returned a null pointer"
);
REQUIRE(program->statements.size() >= n_good_statements);
// ^^ because even though you were
// thinking about a specific number of statements to be there, it
// failing for `expect_next` might trigger a sub-expression to be
// triggered correctly and be parsed as the expression_stmt
}
} // namespace test::utils

91
test/utils/utils.hpp Normal file
View File

@@ -0,0 +1,91 @@
#pragma once
#include "ast/ast.hpp"
#include "lexer/lexer.hpp"
#include "object/object.hpp"
#include "parser/parser.hpp"
#include <any>
#include <cxxabi.h>
#include <doctest.h>
#include <memory>
#include <sstream>
namespace test::utils {
void check_parser_errors(const std::vector<ast::error::error*>&);
struct ParserFixture {
std::stringstream input;
std::unique_ptr<lexer::lexer> lexer;
std::unique_ptr<parser::parser> parser;
std::unique_ptr<ast::program> program;
ParserFixture() = default;
virtual void setup(std::string);
};
struct EvalFixture : ParserFixture {
std::unique_ptr<object::object> result;
void setup(std::string);
};
// parser tests
void test_identifier(ast::expression*, std::string);
void test_integer_literal(ast::expression*, int);
void test_boolean_literal(ast::expression*, bool);
void test_literal_expression(ast::expression*, std::any&);
void
test_infix_expression(ast::expression*, std::any, std::string, std::any);
void test_failing_parsing(std::string, std::vector<token::type>, int = 0);
// eval tests
void test_integer_object(object::object*, int);
namespace {
std::string demangle(const char* name) {
int status = 0;
std::unique_ptr<char, decltype(&std::free)> demangled(
abi::__cxa_demangle(name, nullptr, nullptr, &status),
&std::free
);
return (status == 0 && demangled) ? demangled.get() : name;
}
template <typename T, typename Base>
T* cast_impl(Base* base) {
static_assert(
std::is_base_of_v<Base, T>,
"T must be derived from Base"
);
T* t;
REQUIRE_NOTHROW(t = dynamic_cast<T*>(base));
REQUIRE_MESSAGE(
t != nullptr,
"Couldn't cast " * demangle(typeid(*base).name())
* " (given as " * demangle(typeid(Base).name()) * ")"
* " to a " * demangle(typeid(T).name())
);
return t;
}
} // namespace
// Overloads for your known base types
template <typename T>
T* cast(ast::node* stmt) {
return cast_impl<T, ast::node>(stmt);
}
template <typename T>
T* cast(ast::error::error* err) {
return cast_impl<T, ast::error::error>(err);
}
template <typename T>
T* cast(object::object* err) {
return cast_impl<T, object::object>(err);
}
} // namespace test::utils