src/script/parser.cpp

Sat, 22 Aug 2015 13:04:58 +0300

author
Teemu Piippo <crimsondusk64@gmail.com>
date
Sat, 22 Aug 2015 13:04:58 +0300
branch
scripting
changeset 942
afbd122f3eff
parent 925
2f316b57b508
permissions
-rw-r--r--

Commit work on scripting

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2015 Teemu Piippo
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "parser.h"

static const char* TokenNames[] =
{
	"if",
	"then",
	"else",
	"endif",
	"endmacro",
	"macro",
	"for",
	"while",
	"done",
	"do",
	"==",
	"<=",
	">=",
	"&&",
	"||",
	"!=",
	":",
	";",
	".",
	",",
	"=",
	"<",
	">",
	"?",
	"{",
	"}",
	"[",
	"]",
	"(",
	")",
	"-",
	"+",
	"*",
	"/",
	"\\",
	"&",
	"^",
	"|",
	"!",
	"@",
	"#",
	"~",
	"`",
	"%",
	"<variable>",
	"<string>",
	"<symbol>",
	"<number>",
	"<any>",
};

//
// -------------------------------------------------------------------------------------------------
//
Script::Parser::Parser(QString text) :
	m_script(text) {}

//
// -------------------------------------------------------------------------------------------------
//
Script::Parser::~Parser() {}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::parse()
{
	preprocess();
	m_state.reset();
	m_astRoot = Ast::spawnRoot();

	while (next())
	{
		if (m_state.token.type == TOK_Semicolon)
			continue;

		tokenMustBe (TOK_Macro);
		mustGetNext (TOK_Symbol);
		Ast::MacroPointer macroAst = Ast::spawn<Ast::MacroNode> (m_astRoot, state().token.text);
		mustGetNext (TOK_Semicolon);

		do
		{
			mustGetNext();
		} while (m_state.token.type != TOK_EndMacro);
	}

	m_astRoot->dump();
}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::preprocess()
{
	bool inString = false;
	bool inComment = false;
	bool inBackslash = false;
	int ln = 1;
	int pos = 0;

	// Preprocess
	for (QChar qch : m_script)
	{
		char ch = qch.toAscii();

		if (not inComment and not inString and ch == '\0')
			scriptError ("bad character %1 in script text on line %2", qch, ln);

		if (not inString)
		{
			if (ch == '\\')
			{
				inBackslash = true;
				continue;
			}

			if (inBackslash and ch != '\n')
				scriptError ("misplaced backslash on line %1", ln);
		}

		if (ch == '"')
			inString ^= 1;

		if (ch == '\n')
		{
			if (inString)
				scriptError ("unterminated string on line %1", ln);

			if (not inBackslash)
			{
				m_data.append (';');
				m_data.append ('\n');
				pos += 2;
				inComment = false;
				m_lineEndings << pos;
				++ln;
			}
			else
				inBackslash = false;

			continue;
		}

		if (ch == '#' and not inString)
		{
			inComment = true;
			continue;
		}

		if (not inComment)
		{
			m_data.append (ch);
			++pos;
		}
	}
}

//
// -------------------------------------------------------------------------------------------------
//
namespace Script
{
	class UnexpectedEOF : public std::exception
	{
		const char* what() const throw()
		{
			return "unexpected EOF";
		}
	};
}

//
// -------------------------------------------------------------------------------------------------
//
char Script::Parser::read()
{
	if (m_state.position >= m_data.length())
		throw UnexpectedEOF();

	char ch = m_data[m_state.position];
	m_state.position++;

	if (m_state.position == m_lineEndings[m_state.lineNumber])
		m_state.lineNumber++;

	return ch;
}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::unread()
{
	if (m_state.position <= 0)
		return;

	if (m_state.lineNumber > 0
		and m_state.position == m_lineEndings[m_state.lineNumber - 1])
	{
		m_state.lineNumber--;
	}

	m_state.position--;
}

//
// -------------------------------------------------------------------------------------------------
//
// Takes a hexadecimal character and returns its numerical value. It is assumed that isxdigit(xd)
// is true (if not, result is undefined).
//
int parseXDigit (char xd)
{
	if (xd >= 'a')
		return xd - 'a';

	if (xd >= 'A')
		return xd - 'A';

	return xd - '0';
}

//
// -------------------------------------------------------------------------------------------------
//
bool Script::Parser::next (TokenType desiredType)
{
	State oldpos = state();
	Token oldtoken = m_state.token;

	if (not getNextToken())
		return false;

	if (desiredType != TOK_Any and m_state.token.type != desiredType)
	{
		// Did not find the token we wanted, revert back
		m_rejectedToken = m_state.token;
		m_state.token = oldtoken;
		setState (oldpos);
		return false;
	}

	return true;
}

//
// -------------------------------------------------------------------------------------------------
//
bool Script::Parser::getNextToken()
{
	try
	{
		m_state.token.text.clear();
		m_state.token.number = 0;
		m_state.token.real = 0.0;
		skipSpace();

		const char* data = m_data.constData() + m_state.position;

		// Does this character start one of our tokens?
		for (int tt = 0; tt <= LastNamedToken; ++tt)
		{
			if (strncmp (data, TokenNames[tt], strlen (TokenNames[tt])) != 0)
				continue;

			m_state.position += strlen (TokenNames[tt]);
			m_state.token.text = QString::fromAscii (TokenNames[tt]);
			m_state.token.type = TokenType (tt);
			return true;
		}

		// Check for number
		if (parseNumber())
			return true;

		// Check for string
		if (*data == '"')
		{
			read();
			parseString();
			return true;
		}

		// Check for variable
		if (*data == '$')
		{
			read();
			m_state.token.text = parseIdentifier();
			m_state.token.type = TOK_Variable;
			return true;
		}

		// Must be a symbol of some sort then
		m_state.token.text = parseIdentifier();
		m_state.token.type = TOK_Symbol;
	}
	catch (UnexpectedEOF)
	{
		return false;
	}

	return true;
}

//
// -------------------------------------------------------------------------------------------------
//
bool Script::Parser::parseNumber()
{
	State pos = state();
	char ch = read();
	unread();
	QString numberString;

	if (not isdigit (ch) and ch != '.')
	{
		setState (pos);
		return false;
	}

	int base = 10;
	bool gotDot = false;

	if (tryMatch ("0x", false))
		base = 16;
	elif (tryMatch ("0b", false))
		base = 2;

	int (*checkFunc)(int) = base == 16 ? isxdigit : isdigit;

	for (int n = 0; not isspace (ch = read()); ++n)
	{
		if (n == 0 && ch == '0')
			base = 8;

		if (ch == '.')
		{
			if (gotDot)
				scriptError ("multiple dots in numeric literal");

			// If reading numbers like 0.1234 where the first number is zero, the parser
			// will initially think the number is octal so we must take that into account here.
			// Note that even if you have numbers like 05.612, it will still be decimal.
			if (base != 10 and base != 8)
				scriptError ("real number constant must be decimal");

			base = 10;
			gotDot = true;
		}
		else if (checkFunc (ch))
		{
			if (base <= 10 and (ch - '0') >= base)
				scriptError ("bad base-%1 numeric literal", base);

			numberString += ch;
		}
		else if (isalpha (ch))
			scriptError ("invalid digit %1 in literal", ch);
		else
			break;
	}

	unread();
	bool ok;

	if (gotDot)
	{
		// Floating point number
		m_state.token.real = numberString.toFloat (&ok);
		m_state.token.number = m_state.token.real;
	}
	else
	{
		// Integral number
		m_state.token.number = numberString.toInt (&ok, base);
		m_state.token.real = m_state.token.number;
	}

	if (ok == false)
		scriptError ("invalid numeric literal '%1'", numberString);

	m_state.token.text = numberString;
	m_state.token.type = TOK_Number;

	return true;
}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::scriptError (QString text)
{
	throw ParseError (text);
}

//
// -------------------------------------------------------------------------------------------------
//
// Checks whether the parser is at the beginning of the given string in the code. The string is
// expected not to have newlines. If true, the parser jumps over the text.
//
bool Script::Parser::tryMatch (const char* text, bool caseSensitive)
{
	assert (strstr (text, "\n") == NULL);
	const char* data = m_data.constData() + m_state.position;
	int (*func) (const char*, const char*) = caseSensitive ? &strcmp : &strcasecmp;

	if ((*func) (data, text) == 0)
	{
		m_state.position += strlen (text);
		return true;
	}

	return false;
}

//
// -------------------------------------------------------------------------------------------------
//
QString Script::Parser::parseEscapeSequence()
{
	char ch = read();
	QString result;

	switch (ch)
	{
	case '"':
		result += "\"";
		break;

	case 'n':
		result += "\n";
		break;

	case 't':
		result += "\t";
		break;

	case '\\':
		result += "\\";
		break;

	case 'x':
	case 'X':
		{
			char n1 = read();
			char n2 = read();

			if (not isxdigit(n1) or not isxdigit(n2))
				scriptError ("bad hexa-decimal character \\x%1%2", n1, n2);

			unsigned char num = parseXDigit(n1) * 16 + parseXDigit(n2);
			result += char (num);
		}
		break;

	default:
		scriptError ("unknown escape sequence \\%1", ch);
	}

	return result;
}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::parseString()
{
	m_state.token.type = TOK_String;
	m_state.token.text.clear();

	try
	{
		char ch;

		while ((ch = read()) != '"')
		{
			if (ch == '\\')
				m_state.token.text += parseEscapeSequence();
			else
				m_state.token.text += ch;
		}
	}
	catch (UnexpectedEOF)
	{
		scriptError ("unterminated string");
	}
}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::skipSpace()
{
	while (isspace (read()))
		;

	unread();
}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::mustGetNext (TokenType desiredType)
{
	if (not next (desiredType))
	{
		scriptError ("Expected %1, got %2",
			TokenNames[m_rejectedToken.type],
			TokenNames[desiredType]);
	}
}

//
// -------------------------------------------------------------------------------------------------
//
bool Script::Parser::peekNext (Token& tok)
{
	State pos = state();

	if (next (TOK_Any))
	{
		tok = m_state.token;
		setState (pos);
		return true;
	}

	return false;
}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::setState (const State& pos)
{
	m_state = pos;
}

//
// -------------------------------------------------------------------------------------------------
//
QString Script::Parser::parseIdentifier()
{
	char ch;
	QString identifier;

	while (not isspace (ch = read()))
	{
		if (isalnum (ch) == false and ch != '_')
			break;

		identifier += QChar::fromAscii (ch);
	}

	unread();
	return identifier;
}

//
// -------------------------------------------------------------------------------------------------
//
void Script::Parser::tokenMustBe (TokenType desiredType)
{
	if (m_state.token.type != desiredType)
	{
		scriptError ("expected %1, got %2",
			TokenNames[desiredType],
			TokenNames[m_state.token.type]);
	}
}

mercurial