The complete source code of IdealIRC
http://www.idealirc.org/
1103 lines
24 KiB
1103 lines
24 KiB
#include "Tokenizer.h"
|
|
#include <iostream>
|
|
#include <locale>
|
|
|
|
namespace TKN
|
|
{
|
|
constexpr auto* Function = "function";
|
|
constexpr auto* Command = "command";
|
|
constexpr auto* If = "if";
|
|
constexpr auto* Else = "else";
|
|
constexpr auto* While = "while";
|
|
constexpr auto* Hook = "hook";
|
|
constexpr auto* Dispatch = "dispatch";
|
|
constexpr auto* True = "true";
|
|
constexpr auto* False = "false";
|
|
constexpr auto* Global = "global";
|
|
constexpr auto* Menu = "menu";
|
|
constexpr auto* Dialog = "dialog";
|
|
}
|
|
|
|
namespace {
|
|
inline char decimalSeparator()
|
|
{
|
|
return std::use_facet<std::numpunct<char>>(std::cout.getloc()).decimal_point();
|
|
}
|
|
}
|
|
|
|
class DataIndex
|
|
{
|
|
const std::string& m_data;
|
|
std::size_t m_idx{ 0 };
|
|
int m_line{ 1 };
|
|
int m_col{ 1 };
|
|
|
|
public:
|
|
explicit DataIndex(const std::string& data)
|
|
: m_data(data)
|
|
{}
|
|
|
|
DataIndex(const DataIndex& other)
|
|
: m_data(other.m_data)
|
|
, m_idx(other.m_idx)
|
|
, m_line(other.m_line)
|
|
, m_col(other.m_col)
|
|
{}
|
|
|
|
DataIndex& operator=(const DataIndex& other)
|
|
{
|
|
m_idx = other.m_idx;
|
|
m_line = other.m_line;
|
|
m_col = other.m_col;
|
|
return *this;
|
|
}
|
|
|
|
void operator++()
|
|
{
|
|
m_idx += 1;
|
|
if (m_data[m_idx] == '\n') {
|
|
++m_line;
|
|
m_col = 0;
|
|
}
|
|
++m_col;
|
|
}
|
|
|
|
bool operator<(std::size_t other)
|
|
{
|
|
return m_idx < other;
|
|
}
|
|
|
|
bool operator>(std::size_t other)
|
|
{
|
|
return m_idx > other;
|
|
}
|
|
|
|
bool operator==(std::size_t other)
|
|
{
|
|
return m_idx == other;
|
|
}
|
|
|
|
std::size_t operator()()
|
|
{
|
|
return m_idx;
|
|
}
|
|
|
|
std::pair<int,int> lineCol() const
|
|
{
|
|
return std::make_pair(m_line, m_col);
|
|
}
|
|
};
|
|
|
|
struct TokenizerPriv
|
|
{
|
|
const std::string& data;
|
|
|
|
std::string buf{ "" };
|
|
std::string errorMsg{ "" };
|
|
|
|
DataIndex idx{ data };
|
|
TokenType curType{ TokenType::Undefined };
|
|
|
|
std::vector<std::unique_ptr<Scope>> scopes{};
|
|
std::vector<std::string> globals{};
|
|
std::vector<Menu> menus{};
|
|
std::vector<Dialog> dialogs{};
|
|
|
|
bool whitespace(bool eofIsError = true);
|
|
bool determineType(bool blankExpressionRelated);
|
|
|
|
bool processExpression(Expression& root);
|
|
bool processStatementList(Scope& scope, bool parameterList = false);
|
|
bool processSubmenu(MenuEntry& submenu);
|
|
bool processDialog(Dialog& dialog);
|
|
template<typename T> bool processDialogAttr(T& dialog_or_widget);
|
|
bool processDialogWidgetTab(Dialog& dialog);
|
|
bool processDialogWidget(Dialog& dialog, DialogWidget::Type wtype);
|
|
|
|
bool keywordChar(char c, bool allowNumbers = true);
|
|
std::pair<std::string, DataIndex> peekNextKeyword(DataIndex idx);
|
|
std::string peekPreviousKeyword(DataIndex idx);
|
|
|
|
bool command();
|
|
bool keyword();
|
|
bool oper();
|
|
bool string();
|
|
bool number();
|
|
void comment();
|
|
};
|
|
|
|
bool TokenizerPriv::whitespace(bool eofIsError)
|
|
{
|
|
for (; idx < data.length(); ++idx) {
|
|
char c = data[idx()];
|
|
if (c == '#') {
|
|
comment();
|
|
if (idx == data.length())
|
|
break;
|
|
c = data[idx()];
|
|
}
|
|
|
|
if (!std::isspace(c))
|
|
return true;
|
|
}
|
|
|
|
if (eofIsError) {
|
|
errorMsg = "Unexpected end-of-file";
|
|
return false;
|
|
}
|
|
else
|
|
return true;
|
|
}
|
|
|
|
bool TokenizerPriv::determineType(bool blankExpressionRelated)
|
|
{
|
|
if (!whitespace()) return false;
|
|
const char c = data[idx()];
|
|
|
|
if (c == '/' && blankExpressionRelated)
|
|
curType = TokenType::Command;
|
|
|
|
else if (c == '&' && blankExpressionRelated)
|
|
curType = TokenType::Reference;
|
|
|
|
else if (keywordChar(c, false))
|
|
curType = TokenType::Keyword;
|
|
|
|
else if (c >= '0' && c <= '9')
|
|
curType = TokenType::NumberLiteral; // RealLiteral will be detected during parsing of this type
|
|
|
|
else if (c == '"')
|
|
curType = TokenType::StringLiteral;
|
|
|
|
else if (c == '{')
|
|
curType = TokenType::Scope;
|
|
|
|
else if (c == '}')
|
|
curType = TokenType::EndScope;
|
|
|
|
else if (c == '(') {
|
|
if (idx > 0) {
|
|
std::string prev = peekPreviousKeyword(idx);
|
|
if (prev.empty()) {
|
|
errorMsg = "Syntax error";
|
|
return false;
|
|
}
|
|
if (keywordChar(prev[0], false))
|
|
curType = TokenType::Function;
|
|
else
|
|
curType = TokenType::Expression;
|
|
}
|
|
else
|
|
curType = TokenType::Expression;
|
|
}
|
|
|
|
else if (c == ')' || c == ']')
|
|
curType = TokenType::EndExpression;
|
|
|
|
else if (c == '+' || c == '-' || c == '*' || c == '/' || c == '!'
|
|
|| c == '&' || c == '|' || c == '<' || c == '>' || c == '=')
|
|
curType = TokenType::Operator;
|
|
|
|
else if (c == ',')
|
|
curType = TokenType::Comma;
|
|
|
|
else if (c == ';')
|
|
curType = TokenType::SemiColon;
|
|
|
|
else {
|
|
curType = TokenType::Undefined;
|
|
errorMsg = "Stray character: ";
|
|
errorMsg.push_back(c);
|
|
return false;
|
|
}
|
|
|
|
/* Peek next keyword for specials like 'if', 'else', etc. */
|
|
if (curType == TokenType::Keyword) {
|
|
auto peek = peekNextKeyword(idx);
|
|
|
|
if (peek.first == TKN::If)
|
|
curType = TokenType::CondIf;
|
|
else if (peek.first == TKN::Else)
|
|
curType = TokenType::CondElse;
|
|
else if (peek.first == TKN::While)
|
|
curType = TokenType::CondWhile;
|
|
else if (peek.first == TKN::Dispatch)
|
|
curType = TokenType::Dispatch;
|
|
else if (peek.first == TKN::False || peek.first == TKN::True) {
|
|
curType = TokenType::BoolLiteral;
|
|
buf = peek.first;
|
|
}
|
|
else if (data[peek.second()] == '[') {
|
|
curType = TokenType::MapKeyword;
|
|
buf = peek.first;
|
|
++peek.second;
|
|
}
|
|
|
|
if (curType != TokenType::Keyword)
|
|
idx = peek.second;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TokenizerPriv::command()
|
|
{
|
|
++idx;
|
|
|
|
for (; idx < data.length(); ++idx) {
|
|
const char c = data[idx()];
|
|
if (c == '\n') break;
|
|
if (c == '\r') continue;
|
|
buf += c;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TokenizerPriv::keyword()
|
|
{
|
|
for (; idx < data.length(); ++idx) {
|
|
const char c = data[idx()];
|
|
if (keywordChar(c)) {
|
|
buf += c;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TokenizerPriv::oper()
|
|
{
|
|
for (; idx < data.length(); ++idx) {
|
|
const char c = data[idx()];
|
|
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '!'
|
|
|| c == '&' || c == '|' || c == '<' || c == '>' || c == '=')
|
|
{
|
|
buf += c;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TokenizerPriv::string()
|
|
{
|
|
for (++idx; idx < data.length(); ++idx) {
|
|
const char c = data[idx()];
|
|
if (c == '"') {
|
|
++idx;
|
|
break;
|
|
}
|
|
if (c == '\\') {
|
|
++idx;
|
|
if (idx() + 1 == data.length()) {
|
|
errorMsg = "Escape on end-of-file";
|
|
return false;
|
|
}
|
|
}
|
|
buf += c;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TokenizerPriv::number()
|
|
{
|
|
bool decimalsep = false;
|
|
for (; idx < data.length(); ++idx) {
|
|
const char c = data[idx()];
|
|
if (c == '.' && decimalsep) {
|
|
errorMsg = "Multiple decimal separators";
|
|
return false;
|
|
}
|
|
if (c >= '0' && c <= '9') {
|
|
buf += c;
|
|
}
|
|
else if (c == '.') {
|
|
buf += decimalSeparator();
|
|
decimalsep = true;
|
|
// Side effect:
|
|
curType = TokenType::RealLiteral;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void TokenizerPriv::comment()
|
|
{
|
|
for (; idx < data.length(); ++idx) {
|
|
if (data[idx()] == '\n')
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool TokenizerPriv::processExpression(Expression& root)
|
|
{
|
|
const TokenType& type = curType;
|
|
|
|
while (idx < data.length()) {
|
|
if (!determineType(root.count() == 0)) return false;
|
|
|
|
switch (type) {
|
|
case TokenType::Command:
|
|
{
|
|
if (!command()) return false;
|
|
if (buf.empty()) {
|
|
errorMsg = "Empty command statement";
|
|
return false;
|
|
}
|
|
const auto lc = idx.lineCol();
|
|
root.addToken(buf, type, lc.first, lc.second);
|
|
buf.clear();
|
|
return true;
|
|
}
|
|
|
|
case TokenType::Keyword: // TODO return as own token type
|
|
if (!keyword()) return false;
|
|
if (buf == "return") {
|
|
{
|
|
const auto lc = idx.lineCol();
|
|
root.addToken(buf, type, lc.first, lc.second);
|
|
}
|
|
buf.clear();
|
|
const auto lc = idx.lineCol();
|
|
Expression& expr = root.addExpression(lc.first, lc.second);
|
|
if (!processExpression(expr)) return false;
|
|
if (expr.count() == 0)
|
|
root.eraseBack();
|
|
}
|
|
|
|
else if (buf == "continue") {
|
|
const auto lc = idx.lineCol();
|
|
root.addToken("", TokenType::Continue, lc.first, lc.second);
|
|
buf.clear();
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] != ';')
|
|
errorMsg = "Expected empty statement after 'continue'.";
|
|
continue;
|
|
}
|
|
|
|
else if (buf == "break") {
|
|
const auto lc = idx.lineCol();
|
|
root.addToken("", TokenType::Break, lc.first, lc.second);
|
|
buf.clear();
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] != ';')
|
|
errorMsg = "Expected empty statement after 'break'.";
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case TokenType::Reference:
|
|
++idx;
|
|
buf.clear();
|
|
if (!keyword()) return false;
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] == '[') { // Reference to array element
|
|
++idx;
|
|
const auto lc = idx.lineCol();
|
|
MapToken& token = root.addMapToken(buf, lc.first, lc.second, /*isRef*/ true);
|
|
buf.clear();
|
|
if (!processExpression(token.subscript())) return false;
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case TokenType::MapKeyword:
|
|
{
|
|
const auto lc = idx.lineCol();
|
|
MapToken& token = root.addMapToken(buf, lc.first, lc.second);
|
|
buf.clear();
|
|
if (!processExpression(token.subscript())) return false;
|
|
}
|
|
continue;
|
|
|
|
case TokenType::Operator:
|
|
if (!oper()) return false;
|
|
if (buf == "=") {
|
|
++idx;
|
|
{
|
|
const auto lc = idx.lineCol();
|
|
root.addToken(buf, type, lc.first, lc.second);
|
|
}
|
|
buf.clear();
|
|
const auto lc = idx.lineCol();
|
|
Expression& expr = root.addExpression(lc.first, lc.second);
|
|
if (!processExpression(expr)) return false;
|
|
if (expr.count() == 0)
|
|
root.eraseBack();
|
|
}
|
|
|
|
/* Check if this is subtraction or unary minus */
|
|
if (buf == "-") {
|
|
if (root.count() == 0 || (root.back().type() == TokenType::Operator))
|
|
continue;
|
|
}
|
|
break;
|
|
|
|
case TokenType::StringLiteral:
|
|
if (!string()) return false;
|
|
break;
|
|
|
|
case TokenType::NumberLiteral:
|
|
// May turn the type into a FloatLiteral
|
|
if (!number()) return false;
|
|
break;
|
|
|
|
case TokenType::BoolLiteral:
|
|
{
|
|
const auto lc = idx.lineCol();
|
|
// buf is peeked and stored via determineType().
|
|
root.addBool(buf == "true", lc.first, lc.second);
|
|
buf.clear();
|
|
continue;
|
|
}
|
|
|
|
case TokenType::Expression:
|
|
{
|
|
++idx;
|
|
const auto lc = idx.lineCol();
|
|
Expression& expr = root.addExpression(lc.first, lc.second);
|
|
if (!processExpression(expr)) return false;
|
|
if (expr.count() == 0)
|
|
root.eraseBack();
|
|
continue;
|
|
}
|
|
|
|
case TokenType::EndScope: [[fallthrough]];
|
|
case TokenType::EndExpression: [[fallthrough]];
|
|
case TokenType::Comma:
|
|
++idx;
|
|
[[fallthrough]];
|
|
|
|
case TokenType::SemiColon:
|
|
return true;
|
|
|
|
case TokenType::Comment:
|
|
comment();
|
|
continue;
|
|
|
|
case TokenType::Function:
|
|
{
|
|
std::string fname;
|
|
{
|
|
Token& fnametk = static_cast<Token&>(root.back());
|
|
fname = fnametk.token();
|
|
root.eraseBack();
|
|
}
|
|
|
|
const auto lc = idx.lineCol();
|
|
FunctionToken& fun = root.addFunction(fname, lc.first, lc.second);
|
|
|
|
++idx;
|
|
if (!processStatementList(fun.parameters(), true)) return false;
|
|
|
|
continue;
|
|
}
|
|
|
|
case TokenType::CondIf:
|
|
{
|
|
if (!whitespace()) return false;
|
|
++idx;
|
|
buf.clear();
|
|
|
|
const auto lc1 = idx.lineCol();
|
|
Expression condition(lc1.first, lc1.second);
|
|
if (!processExpression(condition)) return false;
|
|
|
|
const auto lc2 = idx.lineCol();
|
|
ConditionalScopeToken& scopeTk = root.addIfScope(lc2.first, lc2.second, std::move(condition));
|
|
if (!processStatementList(scopeTk.scope())) return false;
|
|
}
|
|
return true;
|
|
|
|
case TokenType::CondWhile:
|
|
{
|
|
if (!whitespace()) return false;
|
|
++idx;
|
|
buf.clear();
|
|
const auto lc1 = idx.lineCol();
|
|
Expression condition(lc1.first, lc1.second);
|
|
if (!processExpression(condition)) return false;
|
|
|
|
const auto lc2 = idx.lineCol();
|
|
ConditionalScopeToken& scopeTk = root.addWhileScope(lc2.first, lc2.second, std::move(condition));
|
|
if (!processStatementList(scopeTk.scope())) return false;
|
|
}
|
|
return true;
|
|
|
|
case TokenType::CondElse:
|
|
{
|
|
const auto lc = idx.lineCol();
|
|
ConditionalScopeToken& scopeTk = root.addElseScope(lc.first, lc.second);
|
|
buf.clear();
|
|
if (!processStatementList(scopeTk.scope())) return false;
|
|
}
|
|
return true;
|
|
|
|
case TokenType::Dispatch:
|
|
{
|
|
buf.clear();
|
|
if (!whitespace()) return false;
|
|
if (!keyword()) return false;
|
|
const auto lc = idx.lineCol();
|
|
DispatchToken& dispTk = root.addDispatchToken(buf, lc.first, lc.second);
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] == '(') {
|
|
++idx;
|
|
buf.clear();
|
|
if (!processStatementList(dispTk.parameters(), true)) return false;
|
|
++idx;
|
|
}
|
|
buf.clear();
|
|
continue;
|
|
}
|
|
|
|
default:
|
|
errorMsg = "Invalid token";
|
|
return false;
|
|
}
|
|
|
|
if (curType != TokenType::SemiColon) {
|
|
const auto lc = idx.lineCol();
|
|
root.addToken(buf, type, lc.first, lc.second);
|
|
buf.clear();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TokenizerPriv::processStatementList(Scope& scope, bool parameterList)
|
|
{
|
|
if (!whitespace()) return false;
|
|
bool multipleStatements = parameterList;
|
|
if (data[idx()] == '{') {
|
|
multipleStatements = true;
|
|
++idx;
|
|
}
|
|
|
|
while (idx < data.length()) {
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] == '}') {
|
|
++idx;
|
|
return true;
|
|
}
|
|
|
|
const auto lc = idx.lineCol();
|
|
Expression expr(lc.first, lc.second);
|
|
if (!processExpression(expr)) return false;
|
|
if (expr.count() > 0)
|
|
scope.addStatement(std::move(expr));
|
|
|
|
if ((parameterList && curType == TokenType::EndExpression) || curType == TokenType::EndScope)
|
|
return true;
|
|
|
|
if (curType == TokenType::SemiColon) {
|
|
++idx;
|
|
}
|
|
|
|
if (!multipleStatements)
|
|
return true;
|
|
}
|
|
|
|
errorMsg = "Unexpected end-of-file";
|
|
return false;
|
|
}
|
|
|
|
bool TokenizerPriv::processSubmenu(MenuEntry& submenu)
|
|
{
|
|
if (data[idx()] != '{') {
|
|
errorMsg = "Expected a '{' for defining the menu";
|
|
return false;
|
|
}
|
|
++idx;
|
|
while (idx < data.length() - 2) { // -2 so we avoid any reading past data[]
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] == '}') { // End submenu
|
|
++idx;
|
|
return true;
|
|
}
|
|
|
|
if (data[idx()] == 'd') {
|
|
auto kwp = peekNextKeyword(idx);
|
|
if (kwp.first != "divide") {
|
|
errorMsg = "Expected either a submenu, an item or divide";
|
|
return false;
|
|
}
|
|
idx = kwp.second;
|
|
submenu.addDivide();
|
|
continue;
|
|
}
|
|
|
|
if (data[idx()] != '"') {
|
|
errorMsg = "Expected a string constant";
|
|
return false;
|
|
}
|
|
|
|
if (!string()) return false;
|
|
const std::string label = buf;
|
|
buf.clear();
|
|
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] == '=' && data[idx() + 1] == '>') { // Menu item
|
|
if (!oper()) return false;
|
|
buf.clear();
|
|
if (!whitespace()) return false;
|
|
const auto lc = idx.lineCol();
|
|
Scope scope(lc.first, lc.second);
|
|
if (!processStatementList(scope)) return false;
|
|
submenu.addItem(label, std::move(scope));
|
|
}
|
|
else if (data[idx()] == '{') { // Submenu
|
|
MenuEntry& entry = submenu.addSubMenu(label);
|
|
if (!processSubmenu(entry)) return false;
|
|
}
|
|
else {
|
|
errorMsg = "Expected either a submenu, an item or divide";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
errorMsg = "Unexpected end-of-file";
|
|
return false;
|
|
}
|
|
|
|
bool TokenizerPriv::processDialog(Dialog& dialog)
|
|
{
|
|
if (data[idx()] != '{') {
|
|
errorMsg = "Expected a '{' to define dialog attributes";
|
|
return false;
|
|
}
|
|
++idx;
|
|
|
|
while (idx < data.length() - 2) { // -2 so we avoid any reading past data[]
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] == '}') { // End dialog
|
|
++idx;
|
|
return true;
|
|
}
|
|
buf.clear();
|
|
if (!determineType(false)) return false;
|
|
|
|
if (curType != TokenType::Keyword) {
|
|
errorMsg = "Expected a keyword";
|
|
return false;
|
|
}
|
|
|
|
if (!keyword()) return false;
|
|
|
|
auto maybeWidgetType = DialogWidget::strToType(buf);
|
|
if (maybeWidgetType) {
|
|
switch (*maybeWidgetType) {
|
|
case DialogWidget::Type::Tab:
|
|
if (!processDialogWidgetTab(dialog)) return false;
|
|
break;
|
|
default:
|
|
if (!processDialogWidget(dialog, *maybeWidgetType)) return false;
|
|
}
|
|
}
|
|
|
|
else if (buf == "hook") {
|
|
buf.clear();
|
|
if (!determineType(false)) return false;
|
|
if (curType != TokenType::Keyword) {
|
|
errorMsg = "Expected a keyword";
|
|
return false;
|
|
}
|
|
if (!keyword()) return false;
|
|
std::string identifier = buf;
|
|
buf.clear();
|
|
if (!whitespace()) return false;
|
|
if (!oper()) return false;
|
|
if (buf != "=>") {
|
|
errorMsg = "Expected operator '=>' to define a scope for the hook '" + identifier + "'";
|
|
return false;
|
|
}
|
|
if (!whitespace()) return false;
|
|
Scope scope(idx.lineCol());
|
|
buf.clear();
|
|
if (!processStatementList(scope)) return false;
|
|
dialog.addHook(identifier, std::move(scope));
|
|
}
|
|
|
|
else // Not a widget type: declare as an attribute.
|
|
if (!processDialogAttr(dialog)) return false;
|
|
}
|
|
|
|
errorMsg = "Unexpected end-of-file";
|
|
return false;
|
|
}
|
|
|
|
template<typename T>
|
|
bool TokenizerPriv::processDialogAttr(T& dialog_or_widget)
|
|
{
|
|
if (!whitespace()) return false;
|
|
std::string identifier = buf;
|
|
buf.clear();
|
|
if (!oper()) return false;
|
|
if (buf != "=") {
|
|
errorMsg = "Expected a '=' to define the attribute '" + identifier + "'";
|
|
return false;
|
|
}
|
|
if (!whitespace()) return false;
|
|
if (!determineType(false)) return false;
|
|
|
|
ValueHolder value;
|
|
switch (curType) {
|
|
case TokenType::StringLiteral:
|
|
buf.clear();
|
|
if (!string()) return false;
|
|
value = buf;
|
|
break;
|
|
|
|
case TokenType::NumberLiteral:
|
|
buf.clear();
|
|
if (!number()) return false; // side-effect: may change current type to a real number.
|
|
if (curType == TokenType::RealLiteral)
|
|
value = std::stod(buf);
|
|
else
|
|
value = std::stoi(buf);
|
|
break;
|
|
|
|
case TokenType::BoolLiteral:
|
|
value = buf == "true" ? true : false;
|
|
break;
|
|
|
|
default:
|
|
errorMsg = "Expected literal value for defining the attribute '" + identifier + "'";
|
|
return false;
|
|
}
|
|
dialog_or_widget.setDefaultAttr(identifier, value);
|
|
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] != ';') {
|
|
errorMsg = "Expected a ';' to declare the end of attribute '" + identifier + "'";
|
|
return false;
|
|
}
|
|
++idx;
|
|
return true;
|
|
}
|
|
|
|
bool TokenizerPriv::processDialogWidgetTab(Dialog& dialog)
|
|
{
|
|
return false; // TODO this function
|
|
}
|
|
|
|
bool TokenizerPriv::processDialogWidget(Dialog& dialog, DialogWidget::Type wtype)
|
|
{
|
|
if (!whitespace()) return false;
|
|
if (!determineType(false)) return false;
|
|
|
|
if (curType != TokenType::Keyword) {
|
|
errorMsg = "Expected a keyword when declaring a widget in dialog '" + dialog.name() + "'";
|
|
return false;
|
|
}
|
|
|
|
buf.clear();
|
|
if (!keyword()) return false;
|
|
std::string widgetIdentifier = buf;
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] != '{') {
|
|
errorMsg = "Expected a '{' to define widget attributes for widget '" + widgetIdentifier + "' in dialog '" + dialog.name() + "'";
|
|
return false;
|
|
}
|
|
++idx;
|
|
|
|
auto& widget = dialog.addWidget(widgetIdentifier, wtype);
|
|
|
|
while (idx < data.length() - 2) { // -2 so we avoid any reading past data[]
|
|
if (!whitespace()) return false;
|
|
if (data[idx()] == '}') { // End widget attributes
|
|
++idx;
|
|
return true;
|
|
}
|
|
buf.clear();
|
|
if (!determineType(false)) return false;
|
|
|
|
if (curType != TokenType::Keyword) {
|
|
errorMsg = "Expected a keyword";
|
|
return false;
|
|
}
|
|
|
|
if (!keyword()) return false;
|
|
else if (buf == "hook") {
|
|
buf.clear();
|
|
if (!determineType(false)) return false;
|
|
if (curType != TokenType::Keyword) {
|
|
errorMsg = "Expected a keyword";
|
|
return false;
|
|
}
|
|
if (!keyword()) return false;
|
|
std::string hookIdentifier = buf;
|
|
buf.clear();
|
|
if (!whitespace()) return false;
|
|
if (!oper()) return false;
|
|
if (buf != "=>") {
|
|
errorMsg = "Expected operator '=>' to define a scope for the hook '" + hookIdentifier + "'";
|
|
return false;
|
|
}
|
|
if (!whitespace()) return false;
|
|
Scope scope(idx.lineCol());
|
|
buf.clear();
|
|
if (!processStatementList(scope)) return false;
|
|
widget.addHook(hookIdentifier, std::move(scope));
|
|
}
|
|
else if (!processDialogAttr(widget)) return false;
|
|
}
|
|
|
|
errorMsg = "Unexpected end-of-file";
|
|
return false;
|
|
}
|
|
|
|
bool TokenizerPriv::keywordChar(char c, bool allowNumbers)
|
|
{
|
|
if (allowNumbers)
|
|
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_');
|
|
else
|
|
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_');
|
|
}
|
|
|
|
std::pair<std::string, DataIndex> TokenizerPriv::peekNextKeyword(DataIndex idx)
|
|
{
|
|
std::string ret;
|
|
for (; idx < data.length(); ++idx) {
|
|
const char c = data[idx()];
|
|
if (keywordChar(c))
|
|
ret += c;
|
|
else
|
|
break;
|
|
}
|
|
return { ret, idx };
|
|
}
|
|
|
|
std::string TokenizerPriv::peekPreviousKeyword(DataIndex idx)
|
|
{
|
|
std::string ret;
|
|
long i = static_cast<long>(idx()) - 1;
|
|
for (; i >= 0; --i) {
|
|
const char c = data[i];
|
|
if (!keywordChar(c))
|
|
break;
|
|
}
|
|
|
|
if (i < 0)
|
|
i = 0;
|
|
else
|
|
++i;
|
|
|
|
// Here, 'i' will be the first character of the token, and 'idx' the end (exclusive, as STL end).
|
|
return data.substr(i, data.length() - idx() - 1);
|
|
}
|
|
|
|
Tokenizer::Tokenizer(const std::string& input)
|
|
: m_data(input)
|
|
{
|
|
std::cout.imbue(std::locale(""));
|
|
}
|
|
|
|
Tokenizer::Tokenizer(Tokenizer&& other)
|
|
: mp(std::move(other.mp))
|
|
, m_data(std::move(other.m_data))
|
|
{}
|
|
|
|
Tokenizer::~Tokenizer() = default;
|
|
|
|
bool Tokenizer::process()
|
|
{
|
|
mp.reset(new TokenizerPriv{ m_data });
|
|
|
|
while (mp->idx() < m_data.length()) {
|
|
mp->buf.clear();
|
|
if (!nextKeyword()) return false; // Get "block" type
|
|
|
|
if (mp->buf == TKN::Function) {
|
|
if (!processImperativeBlock(ImperativeBlockType::Function))
|
|
return false;
|
|
}
|
|
|
|
else if (mp->buf == TKN::Command) {
|
|
if (!processImperativeBlock(ImperativeBlockType::Command))
|
|
return false;
|
|
}
|
|
|
|
else if (mp->buf == TKN::Hook) {
|
|
if (!processImperativeBlock(ImperativeBlockType::Hook))
|
|
return false;
|
|
}
|
|
|
|
else if (mp->buf == TKN::Global) {
|
|
if (!processGlobals())
|
|
return false;
|
|
}
|
|
|
|
else if (mp->buf == TKN::Menu) {
|
|
if (!processMenu())
|
|
return false;
|
|
}
|
|
|
|
else if (mp->buf == TKN::Dialog) {
|
|
if (!processDialog())
|
|
return false;
|
|
}
|
|
|
|
else if (!mp->buf.empty()) {
|
|
mp->errorMsg = "Undefined keyword: " + mp->buf;
|
|
return false;
|
|
}
|
|
|
|
else if (mp->idx() == m_data.length() - 1) {
|
|
mp->errorMsg = "Undefined error (end of buffer)";
|
|
return false;
|
|
}
|
|
|
|
// TODO
|
|
//else {
|
|
// mp->errorMsg = "Undefined error (empty buffer)";
|
|
// return false;
|
|
//}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Tokenizer::processImperativeBlock(ImperativeBlockType blockType)
|
|
{
|
|
if (!nextKeyword()) return false; // Get function name
|
|
|
|
if (mp->buf.empty()) {
|
|
mp->errorMsg = "Expected identifier";
|
|
return false;
|
|
}
|
|
std::string identifier = mp->buf;
|
|
|
|
if (!mp->whitespace()) return false;
|
|
if (m_data[mp->idx()] != '(') {
|
|
mp->errorMsg = "Expected parameter declaration";
|
|
return false;
|
|
}
|
|
|
|
++mp->idx;
|
|
{
|
|
const auto lc1 = mp->idx.lineCol();
|
|
Scope params(lc1.first, lc1.second);
|
|
mp->buf.clear();
|
|
if (!mp->processStatementList(params, true)) return false; // Read parameters
|
|
if (!mp->whitespace()) return false;
|
|
const auto lc2 = mp->idx.lineCol();
|
|
mp->scopes.emplace_back(new ImperativeScope(lc2.first, lc2.second, blockType, identifier, std::move(params)));
|
|
}
|
|
|
|
Scope& scope = *(mp->scopes.back().get());
|
|
if (!mp->processStatementList(scope)) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Tokenizer::processGlobals()
|
|
{
|
|
mp->buf.clear();
|
|
while (mp->idx < m_data.length()) {
|
|
if (!mp->whitespace()) return false;
|
|
if (!mp->keyword()) return false;
|
|
if (!mp->whitespace()) return false;
|
|
|
|
if (m_data[mp->idx()] == ';') {
|
|
if (!mp->buf.empty())
|
|
mp->globals.emplace_back(mp->buf);
|
|
++mp->idx;
|
|
mp->buf.clear();
|
|
return true;
|
|
}
|
|
|
|
if (m_data[mp->idx()] == ',') {
|
|
++mp->idx;
|
|
}
|
|
else {
|
|
mp->errorMsg = "Expected a comma or semicolon after identifier";
|
|
return false;
|
|
}
|
|
|
|
if (!mp->buf.empty()) {
|
|
mp->globals.emplace_back(mp->buf);
|
|
mp->buf.clear();
|
|
}
|
|
else {
|
|
mp->errorMsg = "Expected an identifier";
|
|
return false;
|
|
}
|
|
}
|
|
mp->errorMsg = "Unexpected end-of-file";
|
|
return false;
|
|
}
|
|
|
|
bool Tokenizer::processMenu()
|
|
{
|
|
mp->buf.clear();
|
|
if (!mp->whitespace()) return false;
|
|
if (!mp->keyword()) return false;
|
|
std::string menuName = mp->buf;
|
|
mp->buf.clear();
|
|
mp->menus.emplace_back(menuName);
|
|
Menu& menu = mp->menus.back();
|
|
if (!mp->whitespace()) return false;
|
|
return mp->processSubmenu(menu.content());
|
|
}
|
|
|
|
bool Tokenizer::processDialog()
|
|
{
|
|
mp->buf.clear();
|
|
if (!mp->whitespace()) return false;
|
|
if (!mp->keyword()) return false;
|
|
std::string dialogName = mp->buf;
|
|
mp->buf.clear();
|
|
mp->dialogs.emplace_back(dialogName);
|
|
Dialog& dialog = mp->dialogs.back();
|
|
if (!mp->whitespace()) return false;
|
|
return mp->processDialog(dialog);
|
|
}
|
|
|
|
const std::string& Tokenizer::lastError()
|
|
{
|
|
return mp->errorMsg;
|
|
}
|
|
|
|
std::pair<int, int> Tokenizer::currentLineCol() const
|
|
{
|
|
return mp->idx.lineCol();
|
|
}
|
|
|
|
bool Tokenizer::nextKeyword()
|
|
{
|
|
mp->buf.clear();
|
|
if (!mp->whitespace(/*eofIsError*/ false)) return true;
|
|
if (!mp->keyword()) return false;
|
|
return true;
|
|
}
|
|
|
|
Tokenizer::Iterator Tokenizer::begin() const
|
|
{
|
|
return mp->scopes.begin();
|
|
}
|
|
|
|
Tokenizer::Iterator Tokenizer::end() const
|
|
{
|
|
return mp->scopes.end();
|
|
}
|
|
|
|
const std::vector<std::string>& Tokenizer::globals()
|
|
{
|
|
return mp->globals;
|
|
}
|
|
|
|
const std::vector<Menu>& Tokenizer::menus() const
|
|
{
|
|
return mp->menus;
|
|
}
|
|
|
|
const std::vector<Dialog>& Tokenizer::dialogs() const
|
|
{
|
|
return mp->dialogs;
|
|
}
|
|
|