Compare commits

...

32 Commits

Author SHA1 Message Date
Karma Riuk
132dc65240 added some sort of error generation when parsing
errors occur
2025-07-07 15:04:23 +02:00
Karma Riuk
bbac513aa9 checkign that the casting of the let statement
doesn't throw
2025-07-07 15:01:46 +02:00
Karma Riuk
de465b6122 very basic parser of let statements 2025-07-03 13:30:56 +02:00
Karma Riuk
c091f7f021 added execution of valgrind on valgrind target 2025-07-03 11:59:26 +02:00
Karma Riuk
ca74b67bb0 renamed tests to test 2025-07-03 10:10:22 +02:00
Karma Riuk
4da5313db5 using require instead of check to fail fast 2025-07-02 23:12:28 +02:00
Karma Riuk
896b9001c7 added default value to lexer field to avoid
compiler complaints
2025-07-02 23:00:09 +02:00
Karma Riuk
6181fc8d9f added valgrind to targets to check for memory
leaks
2025-07-02 22:59:49 +02:00
Karma Riuk
d328ae60df added clangd config to disable semantic tokens on
operators because seeing new and delete treated as
operators was hurting my eyes
2025-07-02 22:42:21 +02:00
Karma Riuk
3547822d3e forgot the pragma once for the hpps 2025-07-02 11:17:34 +02:00
Karma Riuk
e773cb649f added the current character to the lexer struct
for cleaner structure
2025-07-01 18:59:43 +02:00
Karma Riuk
69bee723a2 implemented very simple repl 2025-07-01 18:43:25 +02:00
Karma Riuk
aee7a741b1 added EQ and NEQ 2025-07-01 18:01:45 +02:00
Karma Riuk
7973f7522c extended lexer to new keywords 2025-06-30 00:36:31 +02:00
Karma Riuk
5cc7147909 extended single char tokens 2025-06-30 00:27:30 +02:00
Karma Riuk
dec93f8272 implemented lexer for a more complex subset of the
monkey language
2025-06-30 00:12:28 +02:00
Karma Riuk
69217fdf90 added test for full lexer (missing impl) 2025-06-29 20:28:53 +02:00
Karma Riuk
c322b69590 renamed IDENT to IDENTIFIER because i kept reading
indent
2025-06-29 20:04:20 +02:00
Karma Riuk
ffff13b2e0 lexer can now read single character tokens 2025-06-29 12:33:37 +02:00
Karma Riuk
ca05c3577a renamed EOF_ to END_OF_FILE 2025-06-29 12:33:09 +02:00
Karma Riuk
1c928616a4 written structure and tests for lexer, missing
implementation
2025-06-29 10:56:51 +02:00
Karma Riuk
ccfc3ed0f7 fixed bug 2025-06-29 10:56:32 +02:00
Karma Riuk
2aff81ba4c fixed token header and made the tokenTypeStrings
not seeable from outside modules
2025-06-29 10:43:12 +02:00
Karma Riuk
9ad9a0b85b added src to inclusion for lsp 2025-06-29 10:14:29 +02:00
Karma Riuk
09a0dc7b6d brought back namespaces because i think i get it
now
2025-06-29 10:14:04 +02:00
Karma Riuk
65792464bb changed make rule name 2025-06-29 10:13:27 +02:00
Karma Riuk
4771aa4f10 removed namespace perche mi rompeva le palle 2025-06-29 10:07:12 +02:00
Karma Riuk
81cdd0690d made the token type less repetitive 2025-06-28 18:05:01 +02:00
Karma Riuk
4364afa111 added compile_flags.txt for lsp 2025-06-28 17:59:14 +02:00
Karma Riuk
9a13de97e1 initial code 2025-06-28 17:59:08 +02:00
Karma Riuk
8acce0f6a6 made makefile better 2025-06-28 17:57:44 +02:00
Karma Riuk
b966b6dfab put back the tabs because makefiles are bitchy 2025-06-28 17:27:56 +02:00
23 changed files with 880 additions and 60 deletions

2
.clangd Normal file
View File

@@ -0,0 +1,2 @@
SemanticTokens:
DisabledKinds: [Operator]

128
Makefile
View File

@@ -1,70 +1,78 @@
# ────────────────────────────────────
# Compiler and flags
CXX := g++
CXXFLAGS := -Wall -WError -I./include
# ────────────────────────────────────
# Paths
# -------------------------------------------------------------------
# Projectwide settings
# -------------------------------------------------------------------
CXX := g++
CXXFLAGS := -std=c++17 -Wall -Wextra -Iinclude -Isrc -MMD -MP
LDFLAGS :=
SRC_DIR := src
TEST_SRC := test/test.cpp
REPL_SRC := $(SRC_DIR)/main.cpp
TEST_DIR := test
BUILD_DIR := build
OBJ_DIR := $(BUILD_DIR)/objs
BIN_DIR := $(BUILD_DIR)/bin
OBJ_DIR := build/obj
BIN_DIR := build/bin
# -------------------------------------------------------------------
# Source & object lists
# -------------------------------------------------------------------
SRC_CPP := $(shell find $(SRC_DIR) -name '*.cpp')
TEST_CPP := $(shell find $(TEST_DIR) -name '*.cpp')
OBJ := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRC_CPP))
TEST_OBJ := $(patsubst $(TEST_DIR)/%.cpp,$(OBJ_DIR)/test/%.o,$(TEST_CPP))
DEPFILES := $(OBJ:.o=.d) $(TEST_OBJ:.o=.d)
# ────────────────────────────────────
# Source listings
# All .cpp under src/, but exclude your REPL main
LIB_SRCS := $(filter-out $(REPL_SRC),$(shell find $(SRC_DIR) -name '*.cpp'))
# Mirror src/.../*.cpp → build/obj/src/.../*.o
LIB_OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/$(SRC_DIR)/%.o,$(LIB_SRCS))
# Identify your “real” main.cpp so we can exclude it from tests
MAIN_SRC := $(SRC_DIR)/main.cpp
MAIN_OBJ := $(MAIN_SRC:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o)
SRC_OBJS_NO_MAIN := $(filter-out $(MAIN_OBJ),$(OBJ))
# Binaries
TEST_BIN := $(BIN_DIR)/tests
REPL_BIN := $(BIN_DIR)/repl
TARGET := $(BIN_DIR)/monkey
TEST_TARGET := $(BIN_DIR)/monkey_tests
# ────────────────────────────────────
# Default target: build & run tests
all: test
# -------------------------------------------------------------------
# Toplevel rules
# -------------------------------------------------------------------
.PHONY: all clean run tests valgrind
all: $(TARGET) $(TEST_TARGET)
# ─ Link test runner (test.cpp defines main via DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN)
$(TEST_BIN): $(LIB_OBJS) | $(BIN_DIR)
@echo "⏳ Linking tests..."
$(CXX) $(CXXFLAGS) $(TEST_SRC) $(LIB_OBJS) -o $@
valgrind: CXXFLAGS += -O0 -g
valgrind: $(TEST_TARGET)
valgrind -s --leak-check=full --show-leak-kinds=all --track-origins=yes $(TEST_TARGET)
# ─ Link REPL
$(REPL_BIN): $(LIB_OBJS) | $(BIN_DIR)
@echo "🚀 Linking REPL..."
$(CXX) $(CXXFLAGS) $(REPL_SRC) $(LIB_OBJS) -o $@
# ─ Compile each library .cpp → mirrored .o
$(OBJ_DIR)/$(SRC_DIR)/%.o: $(SRC_DIR)/%.cpp
@echo "🛠 Compiling $<"
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
# ────────────────────────────────────
# Run or launch targets
.PHONY: test repl clean all
test: $(TEST_BIN)
@echo "\n✅ Running tests..."
@$(TEST_BIN)
# @$(TEST_BIN) $(if $(TESTCASE),--test-case=$(TESTCASE))
repl: $(REPL_BIN)
@echo "\n🔧 Starting REPL..."
@$(REPL_BIN)
# ────────────────────────────────────
# Ensure bin/ exists before linking
$(BIN_DIR):
@mkdir -p $@
# ────────────────────────────────────
# Clean up everything
clean:
@echo "🧹 Cleaning build artifacts"
@rm -rf $(OBJ_DIR) $(BIN_DIR)
@rm -rf $(BUILD_DIR)
# -------------------------------------------------------------------
# Build & run
# -------------------------------------------------------------------
run: $(TARGET)
@$(TARGET)
test: $(TEST_TARGET)
@$(TEST_TARGET) $(if $(TEST),--test-case=$(TEST))
# -------------------------------------------------------------------
# Link binaries
# -------------------------------------------------------------------
$(TARGET): $(OBJ)
@mkdir -p $(BIN_DIR)
$(CXX) $(LDFLAGS) $^ -o $@
$(TEST_TARGET): $(SRC_OBJS_NO_MAIN) $(TEST_OBJ)
@mkdir -p $(BIN_DIR)
$(CXX) $(LDFLAGS) $^ -o $@
# -------------------------------------------------------------------
# Compile rules
# -------------------------------------------------------------------
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
@mkdir -p $(@D)
$(CXX) $(CXXFLAGS) -c $< -o $@
$(OBJ_DIR)/test/%.o: $(TEST_DIR)/%.cpp
@mkdir -p $(@D)
$(CXX) $(CXXFLAGS) -c $< -o $@
# -------------------------------------------------------------------
# Autoinclude dependencies
# -------------------------------------------------------------------
-include $(DEPFILES)

2
compile_flags.txt Normal file
View File

@@ -0,0 +1,2 @@
-I./include
-I./src

9
src/ast/ast.cpp Normal file
View File

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

29
src/ast/ast.hpp Normal file
View File

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

25
src/ast/errors/error.hpp Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include "token/type.hpp"
namespace ast::error {
struct error : public std::runtime_error {
explicit error(const std::string& message)
: std::runtime_error(message) {}
};
struct parser_error : error {
explicit parser_error(const std::string& message): error(message) {}
};
struct expected_next : parser_error {
token::type expected_type;
explicit expected_next(
token::type expected_type, const std::string& message
)
: parser_error(message),
expected_type(expected_type) {}
};
} // namespace ast::error

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
#include "let.hpp"
namespace ast {
let::let(token::token token)
: token(std::move(token)),
name(nullptr),
value(nullptr) {}
std::string let::token_literal() const {
return token.literal;
}
let::~let() {
delete name;
delete value;
};
} // namespace ast

View File

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

83
src/lexer/lexer.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "lexer.hpp"
#include "token/token.hpp"
#include "token/type.hpp"
#include <cctype>
#include <iostream>
namespace lexer {
token::token lexer::next_token() {
if (!(input >> c))
return {token::type::END_OF_FILE, ""};
switch (c) {
case '=':
if (input.peek() == '=')
return {token::type::EQ, std::string{c, (char) input.get()}};
return {token::type::ASSIGN, c};
case '+':
return {token::type::PLUS, c};
case '-':
return {token::type::MINUS, c};
case '!':
if (input.peek() == '=')
return {token::type::NEQ, std::string{c, (char) input.get()}};
return {token::type::BANG, c};
case '*':
return {token::type::ASTERISK, c};
case '/':
return {token::type::SLASH, c};
case '<':
return {token::type::LT, c};
case '>':
return {token::type::GT, c};
case ',':
return {token::type::COMMA, c};
case ';':
return {token::type::SEMICOLON, c};
case '(':
return {token::type::LPAREN, c};
case ')':
return {token::type::RPAREN, c};
case '{':
return {token::type::LBRACE, c};
case '}':
return {token::type::RBRACE, c};
default:
if (is_letter(c)) {
std::string identifier_or_keyword = read_string();
return {
token::lookup_identifier(identifier_or_keyword),
identifier_or_keyword
};
}
if (std::isdigit(c))
return {token::type::INT, read_int()};
return {token::type::ILLEGAL, c};
}
}
bool lexer::is_letter(char c) {
return c == '_' || std::isalpha(static_cast<unsigned char>(c));
}
std::string lexer::read_string() {
std::string result;
result.push_back(c);
for (char c = input.peek(); is_letter(c); c = input.peek())
result.push_back(input.get());
return result;
}
std::string lexer::read_int() {
std::string result;
result.push_back(c);
for (char c = input.peek(); std::isdigit(c); c = input.peek())
result.push_back(input.get());
return result;
}
} // namespace lexer

18
src/lexer/lexer.hpp Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
#include "token/token.hpp"
#include <istream>
namespace lexer {
struct lexer {
std::istream& input;
char c = 0;
token::token next_token();
private:
bool is_letter(char);
std::string read_string();
std::string read_int();
};
} // namespace lexer

8
src/main.cpp Normal file
View File

@@ -0,0 +1,8 @@
#include "repl/repl.hpp"
#include <iostream>
int main() {
repl::start(std::cin, std::cout);
return 0;
}

84
src/parser/parser.cpp Normal file
View File

@@ -0,0 +1,84 @@
#include "parser.hpp"
#include "ast/errors/error.hpp"
#include "token/type.hpp"
#include <sstream>
namespace parser {
parser::parser(lexer::lexer& lexer)
: lexer(lexer),
current(token::type::ILLEGAL, ""),
next(token::type::ILLEGAL, "") {
next_token();
next_token();
}
void parser::next_token() {
current = next;
next = lexer.next_token();
}
ast::program* parser::parse_program() {
ast::program* p = new ast::program();
for (; current.type != token::type::END_OF_FILE; next_token()) {
ast::statement* stmt = parse_statement();
if (stmt != nullptr)
p->statements.push_back(stmt);
}
return p;
}
ast::statement* parser::parse_statement() {
switch (current.type) {
case token::type::LET:
return parse_let();
default:
return nullptr;
}
}
bool parser::expect_next(token::type t) {
if (next.type == t) {
next_token();
return true;
}
next_error(t);
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) {
std::stringstream ss;
ss << "Expected next token to be " << t << " but instead got "
<< next.type;
errors.push_back(new ast::error::expected_next(t, ss.str()));
}
parser::~parser() {
for (const auto& e : errors)
delete e;
}
} // namespace parser

27
src/parser/parser.hpp Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include "ast/ast.hpp"
#include "ast/errors/error.hpp"
#include "ast/statements/let.hpp"
#include "lexer/lexer.hpp"
#include "token/token.hpp"
namespace parser {
struct parser {
parser(lexer::lexer& lexer);
~parser();
std::vector<ast::error::error*> errors;
ast::program* parse_program();
private:
lexer::lexer& lexer;
token::token current, next;
void next_token();
ast::statement* parse_statement();
ast::let* parse_let();
bool expect_next(token::type);
void next_error(token::type);
};
} // namespace parser

30
src/repl/repl.cpp Normal file
View File

@@ -0,0 +1,30 @@
#include "repl.hpp"
#include "lexer/lexer.hpp"
#include <sstream>
#include <string>
static const std::string PROMPT = ">> ";
namespace repl {
void start(std::istream& in, std::ostream& out) {
while (true) {
out << PROMPT;
std::string line;
if (!std::getline(in, line))
return;
std::istringstream ss(line);
lexer::lexer l{ss};
for (token::token tok = l.next_token();
tok.type != token::type::END_OF_FILE;
tok = l.next_token())
out << tok << " ";
out << std::endl;
}
}
} // namespace repl

8
src/repl/repl.hpp Normal file
View File

@@ -0,0 +1,8 @@
#pragma once
#include <istream>
#include <ostream>
namespace repl {
void start(std::istream&, std::ostream&);
}

20
src/token/token.hpp Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "type.hpp"
#include <string>
namespace token {
struct token {
::token::type type;
std::string literal;
token(::token::type t, std::string s): type(t), literal(s) {}
token(::token::type t, char c): type(t), literal(1, c) {}
};
inline std::ostream& operator<<(std::ostream& os, token tok) {
return os << tok.type << '(' << tok.literal << ')';
}
} // namespace token

43
src/token/type.cpp Normal file
View File

@@ -0,0 +1,43 @@
#include "type.hpp"
#include <array>
#include <unordered_map>
namespace token {
// Array mapping enum values to their string representations
constexpr std::
array<std::string_view, static_cast<size_t>(type::RETURN) + 1>
tokenTypeStrings = {
#define X(name, str) str,
TOKEN_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";
}
static std::unordered_map<std::string, type> keywords{
{"fn", type::FUNCTION},
{"let", type::LET},
{"if", type::IF},
{"else", type::ELSE},
{"true", type::TRUE},
{"false", type::FALSE},
{"return", type::RETURN},
};
type lookup_identifier(std::string ident) {
try {
return keywords.at(ident);
} catch (const std::out_of_range&) {
return type::IDENTIFIER;
}
}
} // namespace token

46
src/token/type.hpp Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <ostream>
namespace token {
// X-macro list of token types and their string representations
#define TOKEN_LIST \
X(ILLEGAL, "ILLEGAL") \
X(END_OF_FILE, "EOF") \
X(IDENTIFIER, "IDENTIFIER") \
X(INT, "INT") \
X(ASSIGN, "=") \
X(PLUS, "+") \
X(MINUS, "-") \
X(BANG, "!") \
X(ASTERISK, "*") \
X(SLASH, "/") \
X(LT, "<") \
X(GT, ">") \
X(COMMA, ",") \
X(SEMICOLON, ";") \
X(LPAREN, "(") \
X(RPAREN, ")") \
X(LBRACE, "{") \
X(RBRACE, "}") \
X(LET, "LET") \
X(FUNCTION, "FUNCTION") \
X(IF, "IF") \
X(ELSE, "ELSE") \
X(TRUE, "TRUE") \
X(FALSE, "FALSE") \
X(EQ, "==") \
X(NEQ, "!=") \
X(RETURN, "RETURN")
// Define the TokenType enum using the X-macro
enum class type {
#define X(name, str) name,
TOKEN_LIST
#undef X
};
std::ostream& operator<<(std::ostream&, type);
type lookup_identifier(std::string);
} // namespace token

161
test/lexer.cpp Normal file
View File

@@ -0,0 +1,161 @@
#include "lexer/lexer.hpp"
#include "token/type.hpp"
#include <doctest.h>
#include <sstream>
#include <string>
TEST_CASE("Single character token") {
struct test {
token::type expectedType;
std::string expectedLiteral;
};
std::string input = "=+(){},;";
std::istringstream ss(input);
lexer::lexer l{ss};
test tests[] = {
{token::type::ASSIGN, "="},
{token::type::PLUS, "+"},
{token::type::LPAREN, "("},
{token::type::RPAREN, ")"},
{token::type::LBRACE, "{"},
{token::type::RBRACE, "}"},
{token::type::COMMA, ","},
{token::type::SEMICOLON, ";"},
{token::type::END_OF_FILE, ""},
};
for (const auto& t : tests) {
token::token tok = l.next_token();
REQUIRE(tok.type == t.expectedType);
REQUIRE(tok.literal == t.expectedLiteral);
}
};
TEST_CASE("More tokens") {
struct test {
token::type expectedType;
std::string expectedLiteral;
};
std::istringstream ss("let five = 5;\
let ten = 10;\
let add = fn(x, y) {\
x + y;\
};\
let result = add(five, ten);\
!-/*5;\
5 < 10 > 5;\
\
if (5 < 10) {\
return true;\
} else {\
return false;\
}\
\
10 == 10;\
10 != 9;\
");
lexer::lexer l{ss};
test tests[] = {
// clang-format off
{token::type::LET, "let"},
{token::type::IDENTIFIER, "five"},
{token::type::ASSIGN, "="},
{token::type::INT, "5"},
{token::type::SEMICOLON, ";"},
{token::type::LET, "let"},
{token::type::IDENTIFIER, "ten"},
{token::type::ASSIGN, "="},
{token::type::INT, "10"},
{token::type::SEMICOLON, ";"},
{token::type::LET, "let"},
{token::type::IDENTIFIER, "add"},
{token::type::ASSIGN, "="},
{token::type::FUNCTION, "fn"},
{token::type::LPAREN, "("},
{token::type::IDENTIFIER, "x"},
{token::type::COMMA, ","},
{token::type::IDENTIFIER, "y"},
{token::type::RPAREN, ")"},
{token::type::LBRACE, "{"},
{token::type::IDENTIFIER, "x"},
{token::type::PLUS, "+"},
{token::type::IDENTIFIER, "y"},
{token::type::SEMICOLON, ";"},
{token::type::RBRACE, "}"},
{token::type::SEMICOLON, ";"},
{token::type::LET, "let"},
{token::type::IDENTIFIER, "result"},
{token::type::ASSIGN, "="},
{token::type::IDENTIFIER, "add"},
{token::type::LPAREN, "("},
{token::type::IDENTIFIER, "five"},
{token::type::COMMA, ","},
{token::type::IDENTIFIER, "ten"},
{token::type::RPAREN, ")"},
{token::type::SEMICOLON, ";"},
{token::type::BANG, "!"},
{token::type::MINUS, "-"},
{token::type::SLASH, "/"},
{token::type::ASTERISK, "*"},
{token::type::INT, "5"},
{token::type::SEMICOLON, ";"},
{token::type::INT, "5"},
{token::type::LT, "<"},
{token::type::INT, "10"},
{token::type::GT, ">"},
{token::type::INT, "5"},
{token::type::SEMICOLON, ";"},
{token::type::IF, "if"},
{token::type::LPAREN, "("},
{token::type::INT, "5"},
{token::type::LT, "<"},
{token::type::INT, "10"},
{token::type::RPAREN, ")"},
{token::type::LBRACE, "{"},
{token::type::RETURN, "return"},
{token::type::TRUE, "true"},
{token::type::SEMICOLON, ";"},
{token::type::RBRACE, "}"},
{token::type::ELSE, "else"},
{token::type::LBRACE, "{"},
{token::type::RETURN, "return"},
{token::type::FALSE, "false"},
{token::type::SEMICOLON, ";"},
{token::type::RBRACE, "}"},
{token::type::INT, "10"},
{token::type::EQ, "=="},
{token::type::INT, "10"},
{token::type::SEMICOLON, ";"},
{token::type::INT, "10"},
{token::type::NEQ, "!="},
{token::type::INT, "9"},
{token::type::SEMICOLON, ";"},
// clang-format on
};
for (const auto& t : tests) {
token::token tok = l.next_token();
REQUIRE(tok.type == t.expectedType);
REQUIRE(tok.literal == t.expectedLiteral);
}
};

134
test/parser.cpp Normal file
View File

@@ -0,0 +1,134 @@
#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;
}

20
test/test.cpp Normal file
View File

@@ -0,0 +1,20 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest.h>
int factorial(int number) {
return number <= 1 ? number : factorial(number - 1) * number;
}
TEST_CASE("fact") {
CHECK(factorial(1) == 1);
CHECK(factorial(2) == 2);
CHECK(factorial(3) == 6);
CHECK(factorial(10) == 3628800);
}
TEST_CASE("fact2") {
CHECK(factorial(1) == 1);
CHECK(factorial(2) == 2);
CHECK(factorial(3) == 6);
CHECK(factorial(10) == 3628800);
}