In the previous post I've started writing a tokenizer for my imaginary programming language (which I decided to name markargs; trivia at the end of the post).
Problem definition:
Read the line and divide input into groups and tag them:
+
,-
,=
-> operatorsidentifiers (underscores, non leading numbers are allowed) -> names. Examples of valid names:
variable
,variable1
,first_variable
.Any integer values -> numbers
Since usually they are streamed, the code will try to imitate a stream, especially idiomatic while (stream >> variable)
. It turned out to be weird to implement, since it will need a separate bit for state, whereas I thought I could just cast the stream to bool
and return it. The problem is that for expression a=b+5
, 5
will not be included in the loop, because when it is read, the stream is already exhausted.
I've also added iterator, so that on semantics analysis I could write something like that:
template <typename InputIterator>
std::pair<symbol_table, abstract_syntax_tree> analyze(InputIterator first, InputIterator last)
Code:
main.cpp:
#include <iostream>
#include <vector>
#include <stdexcept>
#include <sstream>
#include <algorithm>
#include "tokenizer.hpp"
std::ostream& operator<<(std::ostream& os, const std::vector<markargs::token>& tokens)
{
std::string initial_expr;
for (const markargs::token& t : tokens)
{
std::string type_str;
switch (t.tp)
{
case markargs::token::token_type::NAME:
type_str = "name";
break;
case markargs::token::token_type::NUMBER:
type_str = "number";
break;
case markargs::token::token_type::OP:
type_str = "operator";
break;
default:
throw std::logic_error("unknown token encountered");
}
os << "token type: " << type_str << '\n'
<< "payload: " << t.payload << "\n--------------------\n";
initial_expr += t.payload;
}
std::cout << "initial expression: " << initial_expr << '\n';
return os;
}
void fixed_tokencount_check()
{
std::istringstream ss("a = b + 5");
std::cout << ss.str() << '\n';
markargs::tokenizer tokenizer(ss);
std::vector<markargs::token> tokens(5);
for (auto& token : tokens)
{
tokenizer >> token;
}
std::vector<markargs::token> correct_answer{
markargs::token{markargs::token::token_type::NAME, "a"},
markargs::token{markargs::token::token_type::OP, "="},
markargs::token{markargs::token::token_type::NAME, "b"},
markargs::token{markargs::token::token_type::OP, "+"},
markargs::token{markargs::token::token_type::NUMBER, "5"}
};
std::cout << tokens;
if (tokens.size() != correct_answer.size() || !std::equal(tokens.begin(), tokens.end(), correct_answer.begin()))
{
throw std::logic_error("tokenizer doesn't work");
}
}
void read_tilend_check()
{
std::string expr_str("a = b + 5");
std::istringstream ss1(expr_str);
std::istringstream ss2(expr_str);
std::cout << ss1.str() << '\n';
markargs::tokenizer tokenizer(ss1);
markargs::tokenizer tokenizer_copy(ss2);
//exhaust tokenizer
std::vector<markargs::token> tokenizer_result;
markargs::token tk;
while (tokenizer >> tk)
{
tokenizer_result.push_back(tk);
}
markargs::tokenizer_iterator first(tokenizer_copy);
markargs::tokenizer_iterator last;
std::vector<markargs::token> tokenizer_it_result{first, last};
std::vector<markargs::token> correct_answer{
markargs::token{markargs::token::token_type::NAME, "a"},
markargs::token{markargs::token::token_type::OP, "="},
markargs::token{markargs::token::token_type::NAME, "b"},
markargs::token{markargs::token::token_type::OP, "+"},
markargs::token{markargs::token::token_type::NUMBER, "5"}
};
std::cout << "tokenizer result:\n" << tokenizer_result << '\n';
std::cout << "tokenizer_iterator result:\n" << tokenizer_it_result << '\n';
if (tokenizer_result.size() != correct_answer.size() ||
!std::equal(tokenizer_result.begin(), tokenizer_result.end(), correct_answer.begin()))
{
throw std::logic_error("tokenizer doesn't work");
}
if (tokenizer_it_result.size() != correct_answer.size() ||
!std::equal(tokenizer_it_result.begin(), tokenizer_it_result.end(), correct_answer.begin()))
{
throw std::logic_error("tokenizer_iterator doesn't work");
}
}
int main()
{
fixed_tokencount_check();
read_tilend_check();
}
token.hpp:
#ifndef COMPILER_TOKEN_HPP
#define COMPILER_TOKEN_HPP
#include <string>
namespace markargs
{
struct token
{
enum class token_type
{
NAME,
NUMBER,
OP,
NONE //for debug purposes
};
token_type tp;
std::string payload;
token() :
tp(token_type::NONE)
{}
token(token_type tp_, const std::string& payload_) :
tp(tp_),
payload(payload_)
{}
bool operator!()
{
return tp == token_type::NONE;
}
};
inline bool operator==(const token& lhs, const token& rhs)
{
return lhs.tp == rhs.tp && lhs.payload == rhs.payload;
}
inline bool operator!=(const token& lhs, const token& rhs)
{
return !(lhs == rhs);
}
}
#endif
tokenizer.hpp:
#ifndef COMPILER_TOKENIZER_HPP
#define COMPILER_TOKENIZER_HPP
#include <iostream>
#include <iterator>
#include "token.hpp"
namespace markargs
{
class tokenizer
{
std::istream* stream;
bool state;
public:
class tokenizer_iterator
{
token tk;
tokenizer* tknizer;
public:
using iterator_category = std::input_iterator_tag;
using value_type = token;
using reference = value_type&;
using pointer = value_type*;
using difference_type = void; //doesn't make much sense
tokenizer_iterator();
explicit tokenizer_iterator(tokenizer& tkzer);
token& operator*();
tokenizer_iterator& operator++();
tokenizer_iterator operator++(int);
friend bool operator!=(const tokenizer_iterator& lhs,
const tokenizer_iterator& rhs);
};
explicit tokenizer(std::istream& is) noexcept;
//simultaneous access to multiple tokenizers
//will have *very* surprising behavior
tokenizer(const tokenizer& other) = delete;
tokenizer& operator=(const tokenizer& other) = delete;
tokenizer(tokenizer&& other) noexcept;
tokenizer& operator=(tokenizer&& other) = delete;
operator bool();
friend tokenizer& operator>>(tokenizer&, token& tk);
private:
token read_identifier();
token read_number();
token read_operator();
bool is_operator(char c) const noexcept;
};
using tokenizer_iterator = tokenizer::tokenizer_iterator;
}
#endif
tokenizer.cpp:
#include "tokenizer.hpp"
#include <memory>
#include <cctype>
namespace markargs
{
tokenizer::tokenizer(std::istream& is) noexcept:
stream(std::addressof(is))
{}
tokenizer::tokenizer(tokenizer&& other) noexcept:
stream(other.stream)
{
other.stream = nullptr;
}
token tokenizer::read_identifier()
{
std::string current_identifier;
char next_char;
//no need to check, since this function is called from gigantic pile of ifs which means next_char is a letter
*stream >> next_char;
current_identifier += next_char;
//numbers in identifiers are allowed if they are not leading
while (*stream >> next_char && (std::isalnum((unsigned char) next_char) || next_char == '_'))
{
current_identifier += next_char;
}
stream->unget(); //sets badbit if eof, so no check for eof
return token{token::token_type::NAME, current_identifier};
}
token tokenizer::read_number()
{
std::string current_number;
char next_char = 0;
while (*stream >> next_char && std::isdigit((unsigned char) next_char))
{
current_number += next_char;
}
stream->unget(); //sets badbit if eof, so no check for eof
return token{token::token_type::NUMBER, current_number};
}
token tokenizer::read_operator()
{
char op;
*stream >> op;
return token{token::token_type::OP, std::string{op}};
}
bool tokenizer::is_operator(char c) const noexcept
{
return c == '+' || c == '-' || c == '=';
}
tokenizer::operator bool()
{
return state;
}
tokenizer& operator>>(tokenizer& t, token& tk)
{
auto& stream = *t.stream;
char next_char;
if (!stream)
{
t.state = false;
return t;
}
//discard spaces
do
{
stream >> next_char;
} while (std::isspace((unsigned char) next_char));
if (!stream)
{
t.state = false;
return t;
}
stream.unget();
if (std::isalpha((unsigned char) next_char) || next_char == '_')
{
tk = t.read_identifier();
}
else if (std::isdigit((unsigned char) next_char))
{
tk = t.read_number();
}
else if (t.is_operator(next_char))
{
tk = t.read_operator();
}
else
{
throw std::invalid_argument("character outside of character set encountered");
}
return t;
}
tokenizer::tokenizer_iterator::tokenizer_iterator():
tknizer(nullptr)
{}
tokenizer::tokenizer_iterator::tokenizer_iterator(tokenizer& tkzer):
tknizer(&tkzer)
{
*tknizer >> tk;
}
tokenizer::tokenizer_iterator& tokenizer::tokenizer_iterator::operator++()
{
tk = token{};
*tknizer >> tk;
if (!tk)
{
tknizer = nullptr;
}
return *this;
}
tokenizer::tokenizer_iterator tokenizer::tokenizer_iterator::operator++(int)
{
auto copy = *this;
++*this;
return copy;
}
token& tokenizer::tokenizer_iterator::operator*()
{
return tk;
}
bool operator!=(const tokenizer_iterator& lhs, const tokenizer_iterator& rhs)
{
return lhs.tknizer != rhs.tknizer; //returning false doesn't mean they're equal!
}
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.2)
project(compiler)
set(CMAKE_CXX_STANDARD 14)
add_executable(tokenizer
src/tokenizer/main.cpp
src/tokenizer/tokenizer.hpp
src/tokenizer/token.hpp
src/tokenizer/syntax_error.hpp
src/tokenizer/tokenizer.cpp)
Bash script to clone & build:
git clone https://github.com/simmplecoder/markargs
cd markargs
git checkout 0e294d8cfb17076fa43c5e971bbdf87997166443
mkdir build
cd build
cmake ..
make
I think code got better compared to the previous post, although probably lines of code increased. The current way makes it harder to make errors compared to the other case. Also, current code does not have semantic analysis yet, so it has less functionality than in the previous post. Though I'm much more convinced that it works without bugs now.
Concerns:
Architecture. Did I model things correctly?
Use of
std::istream
. Theiostream
part of standard library is mostly unexplored for me. Did I use everything in the most efficient way possible?Long namespace name. Does a shorter one come to mind?
Anything else.
Trivia:
The purpose of the programming language is to simplify typing and evaluating long numerical expressions, encountered in physics and other areas where expressions get really long and unreadable pretty quick. The language will allow defining new variables on the fly and define them later, recursively until all symbols are defined. Due to the nature, I called it markargs
, e.g. mark the arguments and worry about them later.