Below is a C++ version of a lexer for the Monkey programming language, originally written in Go, as presented in the book Writing An Interpreter In Go.
LOC: 262. (Excluding the tests.)
Language:
Copy pasting from the book:
Here is how we bind values to names in Monkey:
let age = 1;
let name = "Monkey";
let result = 10 * (20 / 2);
Besides integers, booleans and strings, the Monkey interpreter we’re going to build will also support arrays and hashes. Here’s what binding an array of integers to a name looks like:
let myArray = [1, 2, 3, 4, 5];
And here is a hash, where values are associated with keys:
let thorsten = {"name": "Thorsten", "age": 28};
Accessing the elements in arrays and hashes is done with index expressions:
myArray[0] // => 1
thorsten["name"] // => "Thorsten"
The let statements can also be used to bind functions to names. Here’s a small function that adds two numbers:
let add = fn(a, b) { return a + b; };
But Monkey not only supports return statements. Implicit return values are also possible, which means we can leave out the return if we want to:
let add = fn(a, b) { a + b; };
And calling a function is as easy as you’d expect:
add(1, 2);
A more complex function, such as a fibonacci function that returns the Nth Fibonacci number, might look like this:
let fibonacci = fn(x) {
if (x == 0) {
0
} else {
if (x == 1) {
1
} else {
fibonacci(x - 1) + fibonacci(x - 2);
}
}
};
Note the recursive calls to fibonacci itself! Monkey also supports a special type of functions, called higher order functions. These are functions that take other functions as arguments. Here is an example:
let twice = fn(f, x) { return f(f(x)); };
let addTwo = fn(x) { return x + 2; };
twice(addTwo, 2); // => 6
Directory Structure:
.
├── lexer
│ ├── lexer.cpp
│ └── lexer.hpp
├── LICENSE
├── Makefile
├── README.md
├── repl
│ └── repl.cpp
├── test
│ ├── lexer_test.cpp
└── token
├── token.cpp
└── token.hpp
4 directories, 9 files
Code:
lexer.hpp:
#ifndef LEXER_HPP
#define LEXER_HPP 1
#include <cstddef>
#include <string_view>
#include "../token/token.hpp"
class Lexer {
std::string_view input;
std::size_t pos{};
std::size_t read_pos{};
char ch;
void read_char();
char peek_char() const;
bool is_letter(char c) const;
void skip_whitespace();
std::string_view read_ident();
std::string_view read_int();
std::string_view read_string();
public:
Lexer(const std::string_view &input);
Token next();
};
#endif /* LEXER_HPP */
lexer.cpp:
#include "lexer.hpp"
#include <cctype>
#include <cstddef>
#include <string>
#include <string_view>
void Lexer::read_char()
{
ch = read_pos >= input.length() ? '\0' : input[read_pos];
pos = read_pos++;
}
char Lexer::peek_char() const
{
return read_pos >= input.length() ? '\0' : input[read_pos];
}
bool Lexer::is_letter(char c) const
{
return c == '_' || std::isalpha(static_cast<unsigned char>(c));
}
void Lexer::skip_whitespace()
{
for (; std::isspace(static_cast<unsigned char>(ch)); read_char())
;
}
std::string_view Lexer::read_ident()
{
const std::size_t orig_pos{pos};
for (; is_letter(ch); read_char())
;
return input.substr(orig_pos, pos - orig_pos);
}
std::string_view Lexer::read_int()
{
const std::size_t orig_pos{pos};
for (; isdigit(static_cast<unsigned char>(ch)); read_char())
;
return input.substr(orig_pos, pos - orig_pos);
}
std::string_view Lexer::read_string()
{
/* Monkey doesn't support escape characters.
* XXX: How to signal EOF? Returning an empty string can not be an error.
* Raise an exception?
*/
const std::size_t orig_pos{pos + 1};
do {
read_char();
} while (ch != '"' && ch != '\0');
return input.substr(orig_pos, pos - orig_pos);
}
Lexer::Lexer(const std::string_view &input) : input(input) { read_char(); }
Token Lexer::next()
{
skip_whitespace();
switch (ch) {
case '\0':
return Token(Token::Type::Eof, "");
case '=':
if (peek_char() == '=') {
read_char();
read_char();
return Token(Token::Type::Eq, "==");
}
read_char();
return Token(Token::Type::Assign, "=");
case '+':
read_char();
return Token(Token::Type::Plus, "+");
case '-':
read_char();
return Token(Token::Type::Minus, "-");
case '!':
if (peek_char() == '=') {
read_char();
read_char();
return Token(Token::Type::Not_eq, "!=");
}
read_char();
return Token(Token::Type::Bang, "!");
case '*':
read_char();
return Token(Token::Type::Asterisk, "*");
case '/':
read_char();
return Token(Token::Type::Slash, "/");
case '<':
read_char();
return Token(Token::Type::Lt, "<");
case '>':
read_char();
return Token(Token::Type::Gt, ">");
case ',':
read_char();
return Token(Token::Type::Comma, ",");
case ';':
read_char();
return Token(Token::Type::Semicolon, ";");
case ':':
read_char();
return Token(Token::Type::Colon, ":");
case '(':
read_char();
return Token(Token::Type::Lparen, "(");
case ')':
read_char();
return Token(Token::Type::Rparen, ")");
case '{':
read_char();
return Token(Token::Type::Lbrace, "{");
case '}':
read_char();
return Token(Token::Type::Rbrace, "}");
case '[':
read_char();
return Token(Token::Type::Lbracket, "[");
case ']':
read_char();
return Token(Token::Type::Rbracket, "]");
case '"': {
const std::string_view ident{read_string()};
read_char();
return Token(Token::Type::String, ident);
}
default:
if (is_letter(ch)) {
const std::string_view ident{read_ident()};
return Token(Token::lookup_ident(ident), ident);
} else if (std::isdigit(static_cast<unsigned char>(ch))) {
return Token(Token::Type::Int, read_int());
}
Token t{Token(Token::Type::Illegal, std::string{1, ch})};
read_char();
return t;
}
}
token.hpp:
#ifndef TOKEN_H
#define TOKEN_H 1
#include <string>
#include <string_view>
#include <vector>
/* This solution is vastly superior to any switch case or array based one,
* because it doesn't duplicate the names, making it easy to change the
* enumeration.
*/
#define FOREACH_TOKEN(_) \
_(Illegal) \
_(Eof) \
_(Ident) \
_(Int) \
_(String) \
_(Assign) \
_(Plus) \
_(Minus) \
_(Asterisk) \
_(Slash) \
_(Bang) \
_(Lt) \
_(Gt) \
_(Eq) \
_(Not_eq) \
_(Comma) \
_(Semicolon) \
_(Colon) \
_(Lparen) \
_(Rparen) \
_(Lbrace) \
_(Rbrace) \
_(Lbracket) \
_(Rbracket) \
_(Function) \
_(Let) \
_(True) \
_(False) \
_(If) \
_(Else) \
_(Return) \
#define GEN_ENUM(ENUM) ENUM,
class Token {
// FIXME: This shouldn't be a vector. How do we write an array without
// providing a size?
static const std::vector<std::string_view> token_strs;
static int token_strs_count;
public:
enum class Type { FOREACH_TOKEN(GEN_ENUM) };
Token::Type type;
std::string lit;
Token(Token::Type type, const std::string_view &lit) : type(type), lit(lit)
{
}
static Token::Type lookup_ident(const std::string_view &ident);
static std::string_view to_str(Token::Type t);
};
#undef GEN_ENUM
#endif /* TOKEN_H */
token.cpp:
#include "token.hpp"
#include <string_view>
#include <unordered_map>
#include <vector>
#define GEN_STRING(STRING) #STRING,
const std::vector<std::string_view> Token::token_strs{
FOREACH_TOKEN(GEN_STRING)};
int Token::token_strs_count{static_cast<int>(token_strs.size())};
Token::Type Token::lookup_ident(const std::string_view &ident)
{
static const std::unordered_map<std::string_view, Token::Type> kws{
{"fn", Token::Type::Function}, {"let", Token::Type::Let},
{"true", Token::Type::True}, {"false", Token::Type::False},
{"if", Token::Type::If}, {"else", Token::Type::Else},
{"return", Token::Type::Return},
};
auto it{kws.find(ident)};
return it != kws.end() ? it->second : Token::Type::Ident;
}
std::string_view Token::to_str(Token::Type t)
{
// XXX: Is returning "" an okay decision?
const int idx{static_cast<int>(t)};
return idx < 0 || idx > token_strs_count
? ""
: token_strs[static_cast<std::size_t>(idx)];
}
#undef GEN_STRING
#undef FOREACH_TOKEN
REPL:
repl.cpp:
#include <iostream>
#include <string>
#include <system_error>
#include "../lexer/lexer.hpp"
#include "../token/token.hpp"
int main()
{
std::cout << "Hello! This is the Monkey Programming Language!\n"
<< "Type in commands:\n";
while (true) {
std::cout << ">> " << std::flush;
std::string input{};
if (!std::getline(std::cin, input)) {
if (std::cin.bad()) {
std::cerr
<< "error: failed to read input: "
<< std::error_code{errno, std::generic_category()}.message()
<< ".\n";
return EXIT_FAILURE;
}
break;
}
Lexer l{input};
for (Token t{l.next()}; t.type != Token::Type::Eof; t = l.next()) {
std::cout << "{Type: " << Token::to_str(t.type)
<< ", Literal: " << t.lit << "}\n";
}
}
}
Tests:
And some tests for the lexer; lexer_test.cpp:
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <stddef.h>
#include <string_view>
#include <vector>
#include "../lexer/lexer.hpp"
#include "../token/token.hpp"
static std::size_t tests_run = 0;
static void run_test(const std::string_view &input,
const std::vector<Token> &tests)
{
++tests_run;
fprintf(stderr, "[INFO]: Running test suite %zu...\n", tests_run);
Lexer l{input};
for (std::size_t i = 0; i < std::size(tests); ++i) {
const Token tt = tests[i];
const Token t{l.next()};
if (t.type != tt.type) {
std::cerr << "[FAIL]: tests[" << i + 1 << "] - Token::Type wrong. "
<< "Expected '" << Token::to_str(tt.type) << "'. Got '"
<< Token::to_str(t.type) << "'.\n",
std::exit(EXIT_FAILURE);
}
/* The strncmp() is for illegal tokens; they are non-null terminated,
* whilst the expected tokens are null-terminated.
*/
if (t.lit != tt.lit &&
(t.type == Token::Type::Illegal &&
strncmp(t.lit.c_str(), tt.lit.c_str(), 1) == 0)) {
std::cerr << "[FAIL]: tests[" << i + 1 << "] - Literal wrong. "
<< "Expected '" << tt.lit << "'. Got '" << t.lit
<< "'.\n",
std::exit(EXIT_FAILURE);
}
}
std::cerr << "[INFO]: All tests passed!\n";
}
int main()
{
/* FIXME: These shouldn't need to be a vector either. */
static const std::vector<Token> test_suite1{
{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::Illegal, "#"}, {Token::Type::Eof, ""},
};
run_test("=+(){},;#", test_suite1);
static const std::vector<Token> test_suite2{
{Token::Type::Let, "let"}, {Token::Type::Ident, "five"},
{Token::Type::Assign, "="}, {Token::Type::Int, "5"},
{Token::Type::Semicolon, ";"}, {Token::Type::Let, "let"},
{Token::Type::Ident, "ten"}, {Token::Type::Assign, "="},
{Token::Type::Int, "10"}, {Token::Type::Semicolon, ";"},
{Token::Type::Let, "let"}, {Token::Type::Ident, "add"},
{Token::Type::Assign, "="}, {Token::Type::Function, "fn"},
{Token::Type::Lparen, "("}, {Token::Type::Ident, "x"},
{Token::Type::Comma, ","}, {Token::Type::Ident, "y"},
{Token::Type::Rparen, ")"}, {Token::Type::Lbrace, "{"},
{Token::Type::Ident, "x"}, {Token::Type::Plus, "+"},
{Token::Type::Ident, "y"}, {Token::Type::Semicolon, ";"},
{Token::Type::Rbrace, "}"}, {Token::Type::Semicolon, ";"},
{Token::Type::Let, "let"}, {Token::Type::Ident, "result"},
{Token::Type::Assign, "="}, {Token::Type::Ident, "add"},
{Token::Type::Lparen, "("}, {Token::Type::Ident, "five"},
{Token::Type::Comma, ","}, {Token::Type::Ident, "ten"},
{Token::Type::Rparen, ")"}, {Token::Type::Semicolon, ";"},
{Token::Type::Eof, ""},
};
run_test("let five = 5;"
"let ten = 10;"
"let add = fn(x, y) { x + y; };"
"let result = add(five, ten);",
test_suite2);
static const std::vector<Token> test_suite3{
{Token::Type::Let, "let"},
{Token::Type::Ident, "five"},
{Token::Type::Assign, "="},
{Token::Type::Int, "5"},
{Token::Type::Semicolon, ";"},
{Token::Type::Let, "let"},
{Token::Type::Ident, "ten"},
{Token::Type::Assign, "="},
{Token::Type::Int, "10"},
{Token::Type::Semicolon, ";"},
{Token::Type::Let, "let"},
{Token::Type::Ident, "add"},
{Token::Type::Assign, "="},
{Token::Type::Function, "fn"},
{Token::Type::Lparen, "("},
{Token::Type::Ident, "x"},
{Token::Type::Comma, ","},
{Token::Type::Ident, "y"},
{Token::Type::Rparen, ")"},
{Token::Type::Lbrace, "{"},
{Token::Type::Ident, "x"},
{Token::Type::Plus, "+"},
{Token::Type::Ident, "y"},
{Token::Type::Semicolon, ";"},
{Token::Type::Rbrace, "}"},
{Token::Type::Semicolon, ";"},
{Token::Type::Let, "let"},
{Token::Type::Ident, "result"},
{Token::Type::Assign, "="},
{Token::Type::Ident, "add"},
{Token::Type::Lparen, "("},
{Token::Type::Ident, "five"},
{Token::Type::Comma, ","},
{Token::Type::Ident, "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::Not_eq, "!="},
{Token::Type::Int, "9"},
{Token::Type::Semicolon, ";"},
{Token::Type::String, "\"foobar\""},
{Token::Type::String, "\" foo bar \""},
{Token::Type::Lbracket, "["},
{Token::Type::Int, "1"},
{Token::Type::Comma, ","},
{Token::Type::Int, "2"},
{Token::Type::Rbracket, "]"},
{Token::Type::Semicolon, ";"},
{Token::Type::Lbrace, "{"},
{Token::Type::String, "\"foo\""},
{Token::Type::Colon, ":"},
{Token::Type::String, "\"bar\""},
{Token::Type::Rbrace, "}"},
{Token::Type::Eof, ""},
};
run_test("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;"
"\"foobar\""
"\" foo bar \""
"[1, 2];"
"{\"foo\": \"bar\"}",
test_suite3);
}
Makefile:
CXX := clang++-18
CXXFLAGS += -std=c++17
CXXFLAGS += -g3
CXXFLAGS += -ggdb
CXXFLAGS += -fPIC
CXXFLAGS += -gdwarf-4
CXXFLAGS += -Wall
CXXFLAGS += -Wextra
CXXFLAGS += -Wwrite-strings
CXXFLAGS += -Wno-parentheses
CXXFLAGS += -Wpedantic
CXXFLAGS += -Warray-bounds
CXXFLAGS += -Wconversion
CXXFLAGS += -Wno-unused-function
BINDIR := bin
TEST_BINDIR := test/bin
SRCDIRS := lexer repl token
SRCS := $(filter-out repl/repl.cpp, $(wildcard $(addsuffix /*.cpp, $(SRCDIRS))))
TESTDIR := test
TEST_SRCS := $(wildcard $(TESTDIR)/*.cpp)
TARGET := $(BINDIR)/repl
TEST_TARGET := $(TEST_BINDIR)/lexer_test
all: $(TARGET)
$(TARGET): repl/repl.cpp $(SRCS) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $^
$(TEST_TARGET): $(SRCS) $(TEST_SRCS) | $(TEST_BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $^
test: $(TEST_TARGET)
@echo "Running tests..."
@./$<
$(BINDIR) $(TEST_BINDIR):
mkdir -p $@
rclean:
rm -rf $(BINDIR)
tclean:
rm -rf $(TEST_BINDIR)
fclean_repl:
rm $(TARGET)
fclean_test:
rm $(TEST_TARGET)
.PHONY: all test clean fclean_repl fclean_test
.DELETE_ON_ERROR:
All the code was formatted with:
clang-format --style={"IndentWidth: 4,BreakBeforeBraces: Stroustrup}"
Review Request:
- project structure
- filenaming/code splitting
- naming and design
- makefile
- general coding comments
- anything, everything
For ease, here's the github repository: lexer-cpp.
Run make
to build the REPL (it'll be in the bin
directory), and make test
to build and run the tests.
For the curious, here's the Go version: lexer-go.
The lexer was previously rewritten in C as well: C99 - Lexer for the Monkey Programming Language from the book "Writing An Interpreter In Go"
(Side-note: I am completely new to C++ and OOP, so judge accordingly and feel free to critique, but refrain from suggesting grotesque, highly-obfuscated, and overly-complicated templated and range-based code snippets that would make me run for the hills.)
lex
flex
etc (I am old school so those are the ones I know) but I am sure there are modern versions of the same. \$\endgroup\$