2014-10-20 14:37:04 +00:00
|
|
|
/*
|
|
|
|
This file is part of cpp-ethereum.
|
|
|
|
|
|
|
|
cpp-ethereum 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.
|
|
|
|
|
|
|
|
cpp-ethereum 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 cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
This file is derived from the file "scanner.cc", which was part of the
|
|
|
|
V8 project. The original copyright header follows:
|
|
|
|
|
|
|
|
Copyright 2006-2012, the V8 project authors. All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
|
|
modification, are permitted provided that the following conditions are
|
|
|
|
met:
|
|
|
|
|
|
|
|
* Redistributions of source code must retain the above copyright
|
|
|
|
notice, this list of conditions and the following disclaimer.
|
|
|
|
* Redistributions in binary form must reproduce the above
|
|
|
|
copyright notice, this list of conditions and the following
|
|
|
|
disclaimer in the documentation and/or other materials provided
|
|
|
|
with the distribution.
|
|
|
|
* Neither the name of Google Inc. nor the names of its
|
|
|
|
contributors may be used to endorse or promote products derived
|
|
|
|
from this software without specific prior written permission.
|
|
|
|
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
|
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
|
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
|
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
|
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
|
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
|
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
|
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
|
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
|
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
*/
|
|
|
|
/**
|
|
|
|
* @author Christian <c@ethdev.com>
|
|
|
|
* @date 2014
|
|
|
|
* Solidity scanner.
|
|
|
|
*/
|
2014-10-06 15:13:52 +00:00
|
|
|
|
2014-10-08 18:53:50 +00:00
|
|
|
#include <algorithm>
|
|
|
|
#include <tuple>
|
2014-10-06 15:13:52 +00:00
|
|
|
#include <libsolidity/Scanner.h>
|
|
|
|
|
2014-10-24 17:06:30 +00:00
|
|
|
using namespace std;
|
|
|
|
|
2014-10-16 12:08:54 +00:00
|
|
|
namespace dev
|
|
|
|
{
|
|
|
|
namespace solidity
|
|
|
|
{
|
2014-10-06 15:13:52 +00:00
|
|
|
|
2014-10-16 12:08:54 +00:00
|
|
|
namespace
|
|
|
|
{
|
|
|
|
bool IsDecimalDigit(char c)
|
|
|
|
{
|
|
|
|
return '0' <= c && c <= '9';
|
|
|
|
}
|
|
|
|
bool IsHexDigit(char c)
|
|
|
|
{
|
|
|
|
return IsDecimalDigit(c)
|
|
|
|
|| ('a' <= c && c <= 'f')
|
|
|
|
|| ('A' <= c && c <= 'F');
|
|
|
|
}
|
|
|
|
bool IsLineTerminator(char c)
|
|
|
|
{
|
|
|
|
return c == '\n';
|
|
|
|
}
|
|
|
|
bool IsWhiteSpace(char c)
|
|
|
|
{
|
|
|
|
return c == ' ' || c == '\n' || c == '\t';
|
|
|
|
}
|
|
|
|
bool IsIdentifierStart(char c)
|
|
|
|
{
|
|
|
|
return c == '_' || c == '$' || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z');
|
|
|
|
}
|
|
|
|
bool IsIdentifierPart(char c)
|
|
|
|
{
|
|
|
|
return IsIdentifierStart(c) || IsDecimalDigit(c);
|
|
|
|
}
|
2014-10-09 10:28:37 +00:00
|
|
|
|
2014-10-16 12:08:54 +00:00
|
|
|
int HexValue(char c)
|
|
|
|
{
|
2014-10-20 12:00:37 +00:00
|
|
|
if (c >= '0' && c <= '9')
|
|
|
|
return c - '0';
|
|
|
|
else if (c >= 'a' && c <= 'f')
|
|
|
|
return c - 'a' + 10;
|
|
|
|
else if (c >= 'A' && c <= 'F')
|
|
|
|
return c - 'A' + 10;
|
2014-10-16 12:08:54 +00:00
|
|
|
else return -1;
|
|
|
|
}
|
2014-10-20 12:00:37 +00:00
|
|
|
} // end anonymous namespace
|
2014-10-06 15:13:52 +00:00
|
|
|
|
2014-11-19 15:21:42 +00:00
|
|
|
void Scanner::reset(CharStream const& _source)
|
2014-10-06 15:13:52 +00:00
|
|
|
{
|
2014-11-19 15:21:42 +00:00
|
|
|
bool found_doc_comment;
|
2014-10-09 10:28:37 +00:00
|
|
|
m_source = _source;
|
|
|
|
m_char = m_source.get();
|
|
|
|
skipWhitespace();
|
2014-11-19 15:21:42 +00:00
|
|
|
found_doc_comment = scanToken();
|
|
|
|
next(found_doc_comment);
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-11-05 13:20:56 +00:00
|
|
|
bool Scanner::scanHexByte(char& o_scannedByte)
|
2014-10-06 15:13:52 +00:00
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
char x = 0;
|
2014-11-05 13:20:56 +00:00
|
|
|
for (int i = 0; i < 2; i++)
|
2014-10-16 12:08:54 +00:00
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
int d = HexValue(m_char);
|
2014-10-16 12:08:54 +00:00
|
|
|
if (d < 0)
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
rollback(i);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
x = x * 16 + d;
|
|
|
|
advance();
|
|
|
|
}
|
2014-11-05 13:20:56 +00:00
|
|
|
o_scannedByte = x;
|
2014-10-09 10:28:37 +00:00
|
|
|
return true;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ensure that tokens can be stored in a byte.
|
|
|
|
BOOST_STATIC_ASSERT(Token::NUM_TOKENS <= 0x100);
|
|
|
|
|
2014-11-19 15:21:42 +00:00
|
|
|
Token::Value Scanner::next(bool _change_skipped_comment)
|
2014-10-06 15:13:52 +00:00
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
m_current_token = m_next_token;
|
2014-11-19 15:21:42 +00:00
|
|
|
if (scanToken() || _change_skipped_comment)
|
|
|
|
m_skipped_comment = m_next_skipped_comment;
|
2014-10-09 10:28:37 +00:00
|
|
|
return m_current_token.token;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
2014-10-16 21:49:45 +00:00
|
|
|
Token::Value Scanner::selectToken(char _next, Token::Value _then, Token::Value _else)
|
|
|
|
{
|
|
|
|
advance();
|
|
|
|
if (m_char == _next)
|
2014-10-17 10:52:39 +00:00
|
|
|
return selectToken(_then);
|
2014-10-16 21:49:45 +00:00
|
|
|
else
|
|
|
|
return _else;
|
|
|
|
}
|
|
|
|
|
2014-10-08 18:53:50 +00:00
|
|
|
|
2014-10-06 15:13:52 +00:00
|
|
|
bool Scanner::skipWhitespace()
|
|
|
|
{
|
2014-10-20 11:02:06 +00:00
|
|
|
int const start_position = getSourcePos();
|
2014-10-16 21:49:45 +00:00
|
|
|
while (IsWhiteSpace(m_char))
|
2014-10-16 12:08:54 +00:00
|
|
|
advance();
|
2014-10-09 10:28:37 +00:00
|
|
|
// Return whether or not we skipped any characters.
|
|
|
|
return getSourcePos() != start_position;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Token::Value Scanner::skipSingleLineComment()
|
|
|
|
{
|
2014-10-16 12:08:54 +00:00
|
|
|
// The line terminator at the end of the line is not considered
|
|
|
|
// to be part of the single-line comment; it is recognized
|
|
|
|
// separately by the lexical grammar and becomes part of the
|
|
|
|
// stream of input elements for the syntactic grammar
|
|
|
|
while (advance() && !IsLineTerminator(m_char)) { };
|
|
|
|
return Token::WHITESPACE;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
2014-11-18 17:50:40 +00:00
|
|
|
// For the moment this function simply consumes a single line triple slash doc comment
|
|
|
|
Token::Value Scanner::scanDocumentationComment()
|
|
|
|
{
|
|
|
|
LiteralScope literal(this);
|
|
|
|
advance(); //consume the last '/'
|
|
|
|
while (!isSourcePastEndOfInput() && !IsLineTerminator(m_char))
|
|
|
|
{
|
|
|
|
char c = m_char;
|
|
|
|
advance();
|
2014-11-19 15:21:42 +00:00
|
|
|
addCommentLiteralChar(c);
|
2014-11-18 17:50:40 +00:00
|
|
|
}
|
|
|
|
literal.Complete();
|
|
|
|
return Token::COMMENT_LITERAL;
|
|
|
|
}
|
|
|
|
|
2014-10-06 15:13:52 +00:00
|
|
|
Token::Value Scanner::skipMultiLineComment()
|
|
|
|
{
|
2014-11-05 13:20:56 +00:00
|
|
|
if (asserts(m_char == '*'))
|
|
|
|
BOOST_THROW_EXCEPTION(InternalCompilerError());
|
2014-10-09 10:28:37 +00:00
|
|
|
advance();
|
2014-10-16 12:08:54 +00:00
|
|
|
while (!isSourcePastEndOfInput())
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
char ch = m_char;
|
|
|
|
advance();
|
2014-10-16 21:49:45 +00:00
|
|
|
|
2014-10-09 10:28:37 +00:00
|
|
|
// If we have reached the end of the multi-line comment, we
|
|
|
|
// consume the '/' and insert a whitespace. This way all
|
|
|
|
// multi-line comments are treated as whitespace.
|
2014-10-16 12:08:54 +00:00
|
|
|
if (ch == '*' && m_char == '/')
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
m_char = ' ';
|
|
|
|
return Token::WHITESPACE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Unterminated multi-line comment.
|
|
|
|
return Token::ILLEGAL;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
2014-11-19 15:21:42 +00:00
|
|
|
bool Scanner::scanToken()
|
2014-10-06 15:13:52 +00:00
|
|
|
{
|
2014-11-19 15:21:42 +00:00
|
|
|
bool found_doc_comment = false;
|
2014-10-16 12:08:54 +00:00
|
|
|
m_next_token.literal.clear();
|
|
|
|
Token::Value token;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
// Remember the position of the next token
|
|
|
|
m_next_token.location.start = getSourcePos();
|
|
|
|
switch (m_char)
|
|
|
|
{
|
2014-10-16 21:49:45 +00:00
|
|
|
case '\n': // fall-through
|
2014-10-16 12:08:54 +00:00
|
|
|
case ' ':
|
|
|
|
case '\t':
|
|
|
|
token = selectToken(Token::WHITESPACE);
|
|
|
|
break;
|
|
|
|
case '"':
|
|
|
|
case '\'':
|
|
|
|
token = scanString();
|
|
|
|
break;
|
|
|
|
case '<':
|
|
|
|
// < <= << <<=
|
|
|
|
advance();
|
|
|
|
if (m_char == '=')
|
|
|
|
token = selectToken(Token::LTE);
|
|
|
|
else if (m_char == '<')
|
|
|
|
token = selectToken('=', Token::ASSIGN_SHL, Token::SHL);
|
|
|
|
else
|
|
|
|
token = Token::LT;
|
|
|
|
break;
|
|
|
|
case '>':
|
|
|
|
// > >= >> >>= >>> >>>=
|
|
|
|
advance();
|
|
|
|
if (m_char == '=')
|
|
|
|
token = selectToken(Token::GTE);
|
|
|
|
else if (m_char == '>')
|
|
|
|
{
|
|
|
|
// >> >>= >>> >>>=
|
|
|
|
advance();
|
|
|
|
if (m_char == '=')
|
|
|
|
token = selectToken(Token::ASSIGN_SAR);
|
|
|
|
else if (m_char == '>')
|
|
|
|
token = selectToken('=', Token::ASSIGN_SHR, Token::SHR);
|
|
|
|
else
|
|
|
|
token = Token::SAR;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
token = Token::GT;
|
|
|
|
break;
|
|
|
|
case '=':
|
|
|
|
// = == =>
|
|
|
|
advance();
|
|
|
|
if (m_char == '=')
|
|
|
|
token = selectToken(Token::EQ);
|
|
|
|
else if (m_char == '>')
|
|
|
|
token = selectToken(Token::ARROW);
|
|
|
|
else
|
|
|
|
token = Token::ASSIGN;
|
|
|
|
break;
|
|
|
|
case '!':
|
|
|
|
// ! !=
|
|
|
|
advance();
|
|
|
|
if (m_char == '=')
|
|
|
|
token = selectToken(Token::NE);
|
|
|
|
else
|
|
|
|
token = Token::NOT;
|
|
|
|
break;
|
|
|
|
case '+':
|
|
|
|
// + ++ +=
|
|
|
|
advance();
|
|
|
|
if (m_char == '+')
|
|
|
|
token = selectToken(Token::INC);
|
|
|
|
else if (m_char == '=')
|
|
|
|
token = selectToken(Token::ASSIGN_ADD);
|
|
|
|
else
|
|
|
|
token = Token::ADD;
|
|
|
|
break;
|
|
|
|
case '-':
|
2014-11-05 07:40:21 +00:00
|
|
|
// - -- -= Number
|
2014-10-16 12:08:54 +00:00
|
|
|
advance();
|
|
|
|
if (m_char == '-')
|
|
|
|
{
|
|
|
|
advance();
|
|
|
|
token = Token::DEC;
|
|
|
|
}
|
|
|
|
else if (m_char == '=')
|
|
|
|
token = selectToken(Token::ASSIGN_SUB);
|
2014-11-05 07:40:21 +00:00
|
|
|
else if (m_char == '.' || IsDecimalDigit(m_char))
|
|
|
|
token = scanNumber('-');
|
2014-10-16 12:08:54 +00:00
|
|
|
else
|
|
|
|
token = Token::SUB;
|
|
|
|
break;
|
|
|
|
case '*':
|
|
|
|
// * *=
|
|
|
|
token = selectToken('=', Token::ASSIGN_MUL, Token::MUL);
|
|
|
|
break;
|
|
|
|
case '%':
|
|
|
|
// % %=
|
|
|
|
token = selectToken('=', Token::ASSIGN_MOD, Token::MOD);
|
|
|
|
break;
|
|
|
|
case '/':
|
|
|
|
// / // /* /=
|
|
|
|
advance();
|
|
|
|
if (m_char == '/')
|
2014-11-18 17:50:40 +00:00
|
|
|
{
|
|
|
|
if (!advance()) /* double slash comment directly before EOS */
|
|
|
|
token = Token::WHITESPACE;
|
2014-11-19 15:21:42 +00:00
|
|
|
else if (m_char == '/')
|
|
|
|
{
|
|
|
|
Token::Value comment;
|
|
|
|
m_next_skipped_comment.location.start = getSourcePos();
|
|
|
|
comment = scanDocumentationComment();
|
|
|
|
m_next_skipped_comment.location.end = getSourcePos();
|
|
|
|
m_next_skipped_comment.token = comment;
|
|
|
|
token = Token::WHITESPACE;
|
|
|
|
found_doc_comment = true;
|
|
|
|
}
|
2014-11-18 17:50:40 +00:00
|
|
|
else
|
|
|
|
token = skipSingleLineComment();
|
|
|
|
}
|
2014-10-16 12:08:54 +00:00
|
|
|
else if (m_char == '*')
|
|
|
|
token = skipMultiLineComment();
|
|
|
|
else if (m_char == '=')
|
|
|
|
token = selectToken(Token::ASSIGN_DIV);
|
|
|
|
else
|
|
|
|
token = Token::DIV;
|
|
|
|
break;
|
|
|
|
case '&':
|
|
|
|
// & && &=
|
|
|
|
advance();
|
|
|
|
if (m_char == '&')
|
|
|
|
token = selectToken(Token::AND);
|
|
|
|
else if (m_char == '=')
|
|
|
|
token = selectToken(Token::ASSIGN_BIT_AND);
|
|
|
|
else
|
|
|
|
token = Token::BIT_AND;
|
|
|
|
break;
|
|
|
|
case '|':
|
|
|
|
// | || |=
|
|
|
|
advance();
|
|
|
|
if (m_char == '|')
|
|
|
|
token = selectToken(Token::OR);
|
|
|
|
else if (m_char == '=')
|
|
|
|
token = selectToken(Token::ASSIGN_BIT_OR);
|
|
|
|
else
|
|
|
|
token = Token::BIT_OR;
|
|
|
|
break;
|
|
|
|
case '^':
|
|
|
|
// ^ ^=
|
|
|
|
token = selectToken('=', Token::ASSIGN_BIT_XOR, Token::BIT_XOR);
|
|
|
|
break;
|
|
|
|
case '.':
|
|
|
|
// . Number
|
|
|
|
advance();
|
|
|
|
if (IsDecimalDigit(m_char))
|
2014-11-05 07:40:21 +00:00
|
|
|
token = scanNumber('.');
|
2014-10-16 12:08:54 +00:00
|
|
|
else
|
|
|
|
token = Token::PERIOD;
|
|
|
|
break;
|
|
|
|
case ':':
|
|
|
|
token = selectToken(Token::COLON);
|
|
|
|
break;
|
|
|
|
case ';':
|
|
|
|
token = selectToken(Token::SEMICOLON);
|
|
|
|
break;
|
|
|
|
case ',':
|
|
|
|
token = selectToken(Token::COMMA);
|
|
|
|
break;
|
|
|
|
case '(':
|
|
|
|
token = selectToken(Token::LPAREN);
|
|
|
|
break;
|
|
|
|
case ')':
|
|
|
|
token = selectToken(Token::RPAREN);
|
|
|
|
break;
|
|
|
|
case '[':
|
|
|
|
token = selectToken(Token::LBRACK);
|
|
|
|
break;
|
|
|
|
case ']':
|
|
|
|
token = selectToken(Token::RBRACK);
|
|
|
|
break;
|
|
|
|
case '{':
|
|
|
|
token = selectToken(Token::LBRACE);
|
|
|
|
break;
|
|
|
|
case '}':
|
|
|
|
token = selectToken(Token::RBRACE);
|
|
|
|
break;
|
|
|
|
case '?':
|
|
|
|
token = selectToken(Token::CONDITIONAL);
|
|
|
|
break;
|
|
|
|
case '~':
|
|
|
|
token = selectToken(Token::BIT_NOT);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
if (IsIdentifierStart(m_char))
|
|
|
|
token = scanIdentifierOrKeyword();
|
|
|
|
else if (IsDecimalDigit(m_char))
|
2014-11-05 07:40:21 +00:00
|
|
|
token = scanNumber();
|
2014-10-16 12:08:54 +00:00
|
|
|
else if (skipWhitespace())
|
|
|
|
token = Token::WHITESPACE;
|
|
|
|
else if (isSourcePastEndOfInput())
|
|
|
|
token = Token::EOS;
|
|
|
|
else
|
|
|
|
token = selectToken(Token::ILLEGAL);
|
|
|
|
break;
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
2014-10-16 12:08:54 +00:00
|
|
|
// Continue scanning for tokens as long as we're just skipping
|
|
|
|
// whitespace.
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
2014-10-16 12:08:54 +00:00
|
|
|
while (token == Token::WHITESPACE);
|
|
|
|
m_next_token.location.end = getSourcePos();
|
|
|
|
m_next_token.token = token;
|
2014-11-19 15:21:42 +00:00
|
|
|
|
|
|
|
return found_doc_comment;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool Scanner::scanEscape()
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
char c = m_char;
|
|
|
|
advance();
|
|
|
|
// Skip escaped newlines.
|
|
|
|
if (IsLineTerminator(c))
|
|
|
|
return true;
|
2014-10-16 12:08:54 +00:00
|
|
|
switch (c)
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
case '\'': // fall through
|
2014-10-16 21:49:45 +00:00
|
|
|
case '"': // fall through
|
2014-10-16 12:08:54 +00:00
|
|
|
case '\\':
|
|
|
|
break;
|
2014-10-16 21:49:45 +00:00
|
|
|
case 'b':
|
2014-10-16 12:08:54 +00:00
|
|
|
c = '\b';
|
|
|
|
break;
|
2014-10-16 21:49:45 +00:00
|
|
|
case 'f':
|
2014-10-16 12:08:54 +00:00
|
|
|
c = '\f';
|
|
|
|
break;
|
2014-10-16 21:49:45 +00:00
|
|
|
case 'n':
|
2014-10-16 12:08:54 +00:00
|
|
|
c = '\n';
|
|
|
|
break;
|
2014-10-16 21:49:45 +00:00
|
|
|
case 'r':
|
2014-10-16 12:08:54 +00:00
|
|
|
c = '\r';
|
|
|
|
break;
|
2014-10-16 21:49:45 +00:00
|
|
|
case 't':
|
2014-10-16 12:08:54 +00:00
|
|
|
c = '\t';
|
|
|
|
break;
|
2014-10-16 21:49:45 +00:00
|
|
|
case 'v':
|
2014-10-16 12:08:54 +00:00
|
|
|
c = '\v';
|
|
|
|
break;
|
2014-10-16 21:49:45 +00:00
|
|
|
case 'x':
|
2014-11-05 13:20:56 +00:00
|
|
|
if (!scanHexByte(c))
|
2014-10-20 12:00:37 +00:00
|
|
|
return false;
|
2014-10-09 10:28:37 +00:00
|
|
|
break;
|
|
|
|
}
|
2014-10-16 21:49:45 +00:00
|
|
|
|
2014-10-09 10:28:37 +00:00
|
|
|
addLiteralChar(c);
|
|
|
|
return true;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Token::Value Scanner::scanString()
|
|
|
|
{
|
2014-10-20 11:02:06 +00:00
|
|
|
char const quote = m_char;
|
2014-10-09 10:28:37 +00:00
|
|
|
advance(); // consume quote
|
|
|
|
LiteralScope literal(this);
|
2014-10-16 12:08:54 +00:00
|
|
|
while (m_char != quote && !isSourcePastEndOfInput() && !IsLineTerminator(m_char))
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
char c = m_char;
|
|
|
|
advance();
|
2014-10-16 12:08:54 +00:00
|
|
|
if (c == '\\')
|
|
|
|
{
|
|
|
|
if (isSourcePastEndOfInput() || !scanEscape())
|
|
|
|
return Token::ILLEGAL;
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
2014-10-16 12:08:54 +00:00
|
|
|
else
|
|
|
|
addLiteralChar(c);
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
|
|
|
if (m_char != quote) return Token::ILLEGAL;
|
|
|
|
literal.Complete();
|
|
|
|
advance(); // consume quote
|
|
|
|
return Token::STRING_LITERAL;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Scanner::scanDecimalDigits()
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
while (IsDecimalDigit(m_char))
|
|
|
|
addLiteralCharAndAdvance();
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-11-05 07:40:21 +00:00
|
|
|
Token::Value Scanner::scanNumber(char _charSeen)
|
2014-10-06 15:13:52 +00:00
|
|
|
{
|
2014-11-05 07:40:21 +00:00
|
|
|
enum { DECIMAL, HEX, BINARY } kind = DECIMAL;
|
2014-10-16 12:08:54 +00:00
|
|
|
LiteralScope literal(this);
|
2014-11-05 07:40:21 +00:00
|
|
|
if (_charSeen == '.')
|
2014-10-16 12:08:54 +00:00
|
|
|
{
|
|
|
|
// we have already seen a decimal point of the float
|
|
|
|
addLiteralChar('.');
|
|
|
|
scanDecimalDigits(); // we know we have at least one digit
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2014-11-05 07:40:21 +00:00
|
|
|
if (_charSeen == '-')
|
|
|
|
addLiteralChar('-');
|
2014-10-16 12:08:54 +00:00
|
|
|
// if the first character is '0' we must check for octals and hex
|
|
|
|
if (m_char == '0')
|
|
|
|
{
|
|
|
|
addLiteralCharAndAdvance();
|
2014-11-05 07:40:21 +00:00
|
|
|
// either 0, 0exxx, 0Exxx, 0.xxx or a hex number
|
2014-10-16 12:08:54 +00:00
|
|
|
if (m_char == 'x' || m_char == 'X')
|
|
|
|
{
|
|
|
|
// hex number
|
|
|
|
kind = HEX;
|
|
|
|
addLiteralCharAndAdvance();
|
|
|
|
if (!IsHexDigit(m_char))
|
|
|
|
return Token::ILLEGAL; // we must have at least one hex digit after 'x'/'X'
|
|
|
|
while (IsHexDigit(m_char))
|
|
|
|
addLiteralCharAndAdvance();
|
|
|
|
}
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
2014-10-16 12:08:54 +00:00
|
|
|
// Parse decimal digits and allow trailing fractional part.
|
|
|
|
if (kind == DECIMAL)
|
|
|
|
{
|
|
|
|
scanDecimalDigits(); // optional
|
|
|
|
if (m_char == '.')
|
|
|
|
{
|
|
|
|
addLiteralCharAndAdvance();
|
|
|
|
scanDecimalDigits(); // optional
|
|
|
|
}
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
|
|
|
}
|
2014-10-16 12:08:54 +00:00
|
|
|
// scan exponent, if any
|
|
|
|
if (m_char == 'e' || m_char == 'E')
|
|
|
|
{
|
2014-11-05 13:20:56 +00:00
|
|
|
if (asserts(kind != HEX)) // 'e'/'E' must be scanned as part of the hex number
|
|
|
|
BOOST_THROW_EXCEPTION(InternalCompilerError());
|
2014-10-16 12:08:54 +00:00
|
|
|
if (kind != DECIMAL) return Token::ILLEGAL;
|
|
|
|
// scan exponent
|
2014-10-09 10:28:37 +00:00
|
|
|
addLiteralCharAndAdvance();
|
2014-10-16 12:08:54 +00:00
|
|
|
if (m_char == '+' || m_char == '-')
|
|
|
|
addLiteralCharAndAdvance();
|
|
|
|
if (!IsDecimalDigit(m_char))
|
|
|
|
return Token::ILLEGAL; // we must have at least one decimal digit after 'e'/'E'
|
|
|
|
scanDecimalDigits();
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
2014-10-16 12:08:54 +00:00
|
|
|
// The source character immediately following a numeric literal must
|
|
|
|
// not be an identifier start or a decimal digit; see ECMA-262
|
|
|
|
// section 7.8.3, page 17 (note that we read only one decimal digit
|
|
|
|
// if the value is 0).
|
|
|
|
if (IsDecimalDigit(m_char) || IsIdentifierStart(m_char))
|
|
|
|
return Token::ILLEGAL;
|
|
|
|
literal.Complete();
|
|
|
|
return Token::NUMBER;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Keyword Matcher
|
|
|
|
|
|
|
|
#define KEYWORDS(KEYWORD_GROUP, KEYWORD) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD_GROUP('a') \
|
|
|
|
KEYWORD("address", Token::ADDRESS) \
|
|
|
|
KEYWORD_GROUP('b') \
|
|
|
|
KEYWORD("break", Token::BREAK) \
|
|
|
|
KEYWORD("bool", Token::BOOL) \
|
|
|
|
KEYWORD_GROUP('c') \
|
|
|
|
KEYWORD("case", Token::CASE) \
|
|
|
|
KEYWORD("const", Token::CONST) \
|
|
|
|
KEYWORD("continue", Token::CONTINUE) \
|
|
|
|
KEYWORD("contract", Token::CONTRACT) \
|
|
|
|
KEYWORD_GROUP('d') \
|
|
|
|
KEYWORD("default", Token::DEFAULT) \
|
|
|
|
KEYWORD("delete", Token::DELETE) \
|
|
|
|
KEYWORD("do", Token::DO) \
|
|
|
|
KEYWORD_GROUP('e') \
|
|
|
|
KEYWORD("else", Token::ELSE) \
|
2014-10-17 10:52:39 +00:00
|
|
|
KEYWORD("extends", Token::EXTENDS) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD_GROUP('f') \
|
|
|
|
KEYWORD("false", Token::FALSE_LITERAL) \
|
|
|
|
KEYWORD("for", Token::FOR) \
|
|
|
|
KEYWORD("function", Token::FUNCTION) \
|
|
|
|
KEYWORD_GROUP('h') \
|
|
|
|
KEYWORD("hash", Token::HASH) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("hash8", Token::HASH8) \
|
|
|
|
KEYWORD("hash16", Token::HASH16) \
|
|
|
|
KEYWORD("hash24", Token::HASH24) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("hash32", Token::HASH32) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("hash40", Token::HASH40) \
|
|
|
|
KEYWORD("hash48", Token::HASH48) \
|
|
|
|
KEYWORD("hash56", Token::HASH56) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("hash64", Token::HASH64) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("hash72", Token::HASH72) \
|
|
|
|
KEYWORD("hash80", Token::HASH80) \
|
|
|
|
KEYWORD("hash88", Token::HASH88) \
|
|
|
|
KEYWORD("hash96", Token::HASH96) \
|
|
|
|
KEYWORD("hash104", Token::HASH104) \
|
|
|
|
KEYWORD("hash112", Token::HASH112) \
|
|
|
|
KEYWORD("hash120", Token::HASH120) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("hash128", Token::HASH128) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("hash136", Token::HASH136) \
|
|
|
|
KEYWORD("hash144", Token::HASH144) \
|
|
|
|
KEYWORD("hash152", Token::HASH152) \
|
|
|
|
KEYWORD("hash160", Token::HASH160) \
|
|
|
|
KEYWORD("hash168", Token::HASH168) \
|
|
|
|
KEYWORD("hash178", Token::HASH176) \
|
|
|
|
KEYWORD("hash184", Token::HASH184) \
|
|
|
|
KEYWORD("hash192", Token::HASH192) \
|
|
|
|
KEYWORD("hash200", Token::HASH200) \
|
|
|
|
KEYWORD("hash208", Token::HASH208) \
|
|
|
|
KEYWORD("hash216", Token::HASH216) \
|
|
|
|
KEYWORD("hash224", Token::HASH224) \
|
|
|
|
KEYWORD("hash232", Token::HASH232) \
|
|
|
|
KEYWORD("hash240", Token::HASH240) \
|
|
|
|
KEYWORD("hash248", Token::HASH248) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("hash256", Token::HASH256) \
|
|
|
|
KEYWORD_GROUP('i') \
|
|
|
|
KEYWORD("if", Token::IF) \
|
|
|
|
KEYWORD("in", Token::IN) \
|
|
|
|
KEYWORD("int", Token::INT) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("int8", Token::INT8) \
|
|
|
|
KEYWORD("int16", Token::INT16) \
|
|
|
|
KEYWORD("int24", Token::INT24) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("int32", Token::INT32) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("int40", Token::INT40) \
|
|
|
|
KEYWORD("int48", Token::INT48) \
|
|
|
|
KEYWORD("int56", Token::INT56) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("int64", Token::INT64) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("int72", Token::INT72) \
|
|
|
|
KEYWORD("int80", Token::INT80) \
|
|
|
|
KEYWORD("int88", Token::INT88) \
|
|
|
|
KEYWORD("int96", Token::INT96) \
|
|
|
|
KEYWORD("int104", Token::INT104) \
|
|
|
|
KEYWORD("int112", Token::INT112) \
|
|
|
|
KEYWORD("int120", Token::INT120) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("int128", Token::INT128) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("int136", Token::INT136) \
|
|
|
|
KEYWORD("int144", Token::INT144) \
|
|
|
|
KEYWORD("int152", Token::INT152) \
|
|
|
|
KEYWORD("int160", Token::INT160) \
|
|
|
|
KEYWORD("int168", Token::INT168) \
|
|
|
|
KEYWORD("int178", Token::INT176) \
|
|
|
|
KEYWORD("int184", Token::INT184) \
|
|
|
|
KEYWORD("int192", Token::INT192) \
|
|
|
|
KEYWORD("int200", Token::INT200) \
|
|
|
|
KEYWORD("int208", Token::INT208) \
|
|
|
|
KEYWORD("int216", Token::INT216) \
|
|
|
|
KEYWORD("int224", Token::INT224) \
|
|
|
|
KEYWORD("int232", Token::INT232) \
|
|
|
|
KEYWORD("int240", Token::INT240) \
|
|
|
|
KEYWORD("int248", Token::INT248) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("int256", Token::INT256) \
|
|
|
|
KEYWORD_GROUP('l') \
|
|
|
|
KEYWORD_GROUP('m') \
|
|
|
|
KEYWORD("mapping", Token::MAPPING) \
|
|
|
|
KEYWORD_GROUP('n') \
|
|
|
|
KEYWORD("new", Token::NEW) \
|
|
|
|
KEYWORD("null", Token::NULL_LITERAL) \
|
|
|
|
KEYWORD_GROUP('p') \
|
|
|
|
KEYWORD("private", Token::PRIVATE) \
|
|
|
|
KEYWORD("public", Token::PUBLIC) \
|
|
|
|
KEYWORD_GROUP('r') \
|
|
|
|
KEYWORD("real", Token::REAL) \
|
|
|
|
KEYWORD("return", Token::RETURN) \
|
|
|
|
KEYWORD("returns", Token::RETURNS) \
|
|
|
|
KEYWORD_GROUP('s') \
|
|
|
|
KEYWORD("string", Token::STRING_TYPE) \
|
|
|
|
KEYWORD("struct", Token::STRUCT) \
|
|
|
|
KEYWORD("switch", Token::SWITCH) \
|
|
|
|
KEYWORD_GROUP('t') \
|
|
|
|
KEYWORD("text", Token::TEXT) \
|
|
|
|
KEYWORD("this", Token::THIS) \
|
|
|
|
KEYWORD("true", Token::TRUE_LITERAL) \
|
|
|
|
KEYWORD_GROUP('u') \
|
|
|
|
KEYWORD("uint", Token::UINT) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("uint8", Token::UINT8) \
|
|
|
|
KEYWORD("uint16", Token::UINT16) \
|
|
|
|
KEYWORD("uint24", Token::UINT24) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("uint32", Token::UINT32) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("uint40", Token::UINT40) \
|
|
|
|
KEYWORD("uint48", Token::UINT48) \
|
|
|
|
KEYWORD("uint56", Token::UINT56) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("uint64", Token::UINT64) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("uint72", Token::UINT72) \
|
|
|
|
KEYWORD("uint80", Token::UINT80) \
|
|
|
|
KEYWORD("uint88", Token::UINT88) \
|
|
|
|
KEYWORD("uint96", Token::UINT96) \
|
|
|
|
KEYWORD("uint104", Token::UINT104) \
|
|
|
|
KEYWORD("uint112", Token::UINT112) \
|
|
|
|
KEYWORD("uint120", Token::UINT120) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("uint128", Token::UINT128) \
|
2014-11-05 10:38:26 +00:00
|
|
|
KEYWORD("uint136", Token::UINT136) \
|
|
|
|
KEYWORD("uint144", Token::UINT144) \
|
|
|
|
KEYWORD("uint152", Token::UINT152) \
|
|
|
|
KEYWORD("uint160", Token::UINT160) \
|
|
|
|
KEYWORD("uint168", Token::UINT168) \
|
|
|
|
KEYWORD("uint178", Token::UINT176) \
|
|
|
|
KEYWORD("uint184", Token::UINT184) \
|
|
|
|
KEYWORD("uint192", Token::UINT192) \
|
|
|
|
KEYWORD("uint200", Token::UINT200) \
|
|
|
|
KEYWORD("uint208", Token::UINT208) \
|
|
|
|
KEYWORD("uint216", Token::UINT216) \
|
|
|
|
KEYWORD("uint224", Token::UINT224) \
|
|
|
|
KEYWORD("uint232", Token::UINT232) \
|
|
|
|
KEYWORD("uint240", Token::UINT240) \
|
|
|
|
KEYWORD("uint248", Token::UINT248) \
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORD("uint256", Token::UINT256) \
|
|
|
|
KEYWORD("ureal", Token::UREAL) \
|
|
|
|
KEYWORD_GROUP('v') \
|
|
|
|
KEYWORD("var", Token::VAR) \
|
|
|
|
KEYWORD_GROUP('w') \
|
|
|
|
KEYWORD("while", Token::WHILE) \
|
2014-10-06 15:13:52 +00:00
|
|
|
|
|
|
|
|
2014-10-24 17:06:30 +00:00
|
|
|
static Token::Value KeywordOrIdentifierToken(string const& input)
|
2014-10-06 15:13:52 +00:00
|
|
|
{
|
2014-11-05 13:20:56 +00:00
|
|
|
if (asserts(!input.empty()))
|
|
|
|
BOOST_THROW_EXCEPTION(InternalCompilerError());
|
2014-10-20 11:02:06 +00:00
|
|
|
int const kMinLength = 2;
|
|
|
|
int const kMaxLength = 10;
|
2014-10-16 12:08:54 +00:00
|
|
|
if (input.size() < kMinLength || input.size() > kMaxLength)
|
|
|
|
return Token::IDENTIFIER;
|
|
|
|
switch (input[0])
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
default:
|
2014-10-06 15:13:52 +00:00
|
|
|
#define KEYWORD_GROUP_CASE(ch) \
|
2014-10-16 12:08:54 +00:00
|
|
|
break; \
|
|
|
|
case ch:
|
2014-10-06 15:13:52 +00:00
|
|
|
#define KEYWORD(keyword, token) \
|
2014-10-09 10:28:37 +00:00
|
|
|
{ \
|
2014-10-16 12:08:54 +00:00
|
|
|
/* 'keyword' is a char array, so sizeof(keyword) is */ \
|
|
|
|
/* strlen(keyword) plus 1 for the NUL char. */ \
|
2014-10-20 11:02:06 +00:00
|
|
|
int const keyword_length = sizeof(keyword) - 1; \
|
2014-10-16 12:08:54 +00:00
|
|
|
BOOST_STATIC_ASSERT(keyword_length >= kMinLength); \
|
|
|
|
BOOST_STATIC_ASSERT(keyword_length <= kMaxLength); \
|
2014-10-20 12:00:37 +00:00
|
|
|
if (input == keyword) \
|
|
|
|
return token; \
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
2014-10-16 12:08:54 +00:00
|
|
|
KEYWORDS(KEYWORD_GROUP_CASE, KEYWORD)
|
|
|
|
}
|
|
|
|
return Token::IDENTIFIER;
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Token::Value Scanner::scanIdentifierOrKeyword()
|
|
|
|
{
|
2014-11-05 13:20:56 +00:00
|
|
|
if (asserts(IsIdentifierStart(m_char)))
|
|
|
|
BOOST_THROW_EXCEPTION(InternalCompilerError());
|
2014-10-09 10:28:37 +00:00
|
|
|
LiteralScope literal(this);
|
|
|
|
addLiteralCharAndAdvance();
|
|
|
|
// Scan the rest of the identifier characters.
|
|
|
|
while (IsIdentifierPart(m_char))
|
|
|
|
addLiteralCharAndAdvance();
|
|
|
|
literal.Complete();
|
|
|
|
return KeywordOrIdentifierToken(m_next_token.literal);
|
2014-10-06 15:13:52 +00:00
|
|
|
}
|
|
|
|
|
2014-10-16 21:49:45 +00:00
|
|
|
char CharStream::advanceAndGet()
|
|
|
|
{
|
2014-10-20 12:00:37 +00:00
|
|
|
if (isPastEndOfInput())
|
|
|
|
return 0;
|
2014-10-16 21:49:45 +00:00
|
|
|
++m_pos;
|
2014-10-20 12:00:37 +00:00
|
|
|
if (isPastEndOfInput())
|
|
|
|
return 0;
|
2014-10-16 21:49:45 +00:00
|
|
|
return get();
|
|
|
|
}
|
|
|
|
|
|
|
|
char CharStream::rollback(size_t _amount)
|
|
|
|
{
|
2014-11-05 13:20:56 +00:00
|
|
|
if (asserts(m_pos >= _amount))
|
|
|
|
BOOST_THROW_EXCEPTION(InternalCompilerError());
|
2014-10-16 21:49:45 +00:00
|
|
|
m_pos -= _amount;
|
|
|
|
return get();
|
|
|
|
}
|
|
|
|
|
2014-10-24 17:06:30 +00:00
|
|
|
string CharStream::getLineAtPosition(int _position) const
|
2014-10-08 18:53:50 +00:00
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
// if _position points to \n, it returns the line before the \n
|
2014-10-24 17:06:30 +00:00
|
|
|
using size_type = string::size_type;
|
|
|
|
size_type searchStart = min<size_type>(m_source.size(), _position);
|
2014-10-20 12:00:37 +00:00
|
|
|
if (searchStart > 0)
|
|
|
|
searchStart--;
|
2014-10-09 10:28:37 +00:00
|
|
|
size_type lineStart = m_source.rfind('\n', searchStart);
|
2014-10-24 17:06:30 +00:00
|
|
|
if (lineStart == string::npos)
|
2014-10-09 10:28:37 +00:00
|
|
|
lineStart = 0;
|
|
|
|
else
|
|
|
|
lineStart++;
|
2014-10-24 17:06:30 +00:00
|
|
|
return m_source.substr(lineStart, min(m_source.find('\n', lineStart),
|
|
|
|
m_source.size()) - lineStart);
|
2014-10-08 18:53:50 +00:00
|
|
|
}
|
|
|
|
|
2014-10-24 17:06:30 +00:00
|
|
|
tuple<int, int> CharStream::translatePositionToLineColumn(int _position) const
|
2014-10-08 18:53:50 +00:00
|
|
|
{
|
2014-10-24 17:06:30 +00:00
|
|
|
using size_type = string::size_type;
|
|
|
|
size_type searchPosition = min<size_type>(m_source.size(), _position);
|
|
|
|
int lineNumber = count(m_source.begin(), m_source.begin() + searchPosition, '\n');
|
2014-10-09 10:28:37 +00:00
|
|
|
size_type lineStart;
|
2014-10-16 12:08:54 +00:00
|
|
|
if (searchPosition == 0)
|
2014-10-09 10:28:37 +00:00
|
|
|
lineStart = 0;
|
2014-10-16 12:08:54 +00:00
|
|
|
else
|
|
|
|
{
|
2014-10-09 10:28:37 +00:00
|
|
|
lineStart = m_source.rfind('\n', searchPosition - 1);
|
2014-10-24 17:06:30 +00:00
|
|
|
lineStart = lineStart == string::npos ? 0 : lineStart + 1;
|
2014-10-09 10:28:37 +00:00
|
|
|
}
|
2014-10-24 17:06:30 +00:00
|
|
|
return tuple<int, int>(lineNumber, searchPosition - lineStart);
|
2014-10-08 18:53:50 +00:00
|
|
|
}
|
|
|
|
|
2014-10-06 15:13:52 +00:00
|
|
|
|
2014-10-16 12:08:54 +00:00
|
|
|
}
|
|
|
|
}
|