kicad/common/dsnlexer.cpp

880 lines
22 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2007-2013 SoftPLC Corporation, Dick Hollenbeck <dick@softplc.com>
* Copyright (C) 2007-2024 KiCad Developers, see AUTHORS.txt for contributors.
*
* 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <charconv>
#include <cstdarg>
#include <cstdio>
#include <cstdlib> // bsearch()
#include <cctype>
#include <dsnlexer.h>
#include <wx/translation.h>
#define FMT_CLIPBOARD _( "clipboard" )
//-----<DSNLEXER>-------------------------------------------------------------
void DSNLEXER::init()
{
curTok = DSN_NONE;
prevTok = DSN_NONE;
stringDelimiter = '"';
specctraMode = false;
space_in_quoted_tokens = false;
commentsAreTokens = false;
curOffset = 0;
}
DSNLEXER::DSNLEXER( const KEYWORD* aKeywordTable, unsigned aKeywordCount,
const KEYWORD_MAP* aKeywordMap,
FILE* aFile, const wxString& aFilename ) :
iOwnReaders( true ),
start( nullptr ),
next( nullptr ),
limit( nullptr ),
reader( nullptr ),
keywords( aKeywordTable ),
keywordCount( aKeywordCount ),
keywordsLookup( aKeywordMap )
{
FILE_LINE_READER* fileReader = new FILE_LINE_READER( aFile, aFilename );
PushReader( fileReader );
init();
}
DSNLEXER::DSNLEXER( const KEYWORD* aKeywordTable, unsigned aKeywordCount,
const KEYWORD_MAP* aKeywordMap,
const std::string& aClipboardTxt, const wxString& aSource ) :
iOwnReaders( true ),
start( nullptr ),
next( nullptr ),
limit( nullptr ),
reader( nullptr ),
keywords( aKeywordTable ),
keywordCount( aKeywordCount ),
keywordsLookup( aKeywordMap )
{
STRING_LINE_READER* stringReader = new STRING_LINE_READER( aClipboardTxt, aSource.IsEmpty() ?
wxString( FMT_CLIPBOARD ) : aSource );
PushReader( stringReader );
init();
}
DSNLEXER::DSNLEXER( const KEYWORD* aKeywordTable, unsigned aKeywordCount,
const KEYWORD_MAP* aKeywordMap,
LINE_READER* aLineReader ) :
iOwnReaders( false ),
start( nullptr ),
next( nullptr ),
limit( nullptr ),
reader( nullptr ),
keywords( aKeywordTable ),
keywordCount( aKeywordCount ),
keywordsLookup( aKeywordMap )
{
if( aLineReader )
PushReader( aLineReader );
init();
}
static const KEYWORD empty_keywords[1] = {};
DSNLEXER::DSNLEXER( const std::string& aSExpression, const wxString& aSource ) :
iOwnReaders( true ),
start( nullptr ),
next( nullptr ),
limit( nullptr ),
reader( nullptr ),
keywords( empty_keywords ),
keywordCount( 0 ),
keywordsLookup( nullptr )
{
STRING_LINE_READER* stringReader = new STRING_LINE_READER( aSExpression, aSource.IsEmpty() ?
wxString( FMT_CLIPBOARD ) : aSource );
PushReader( stringReader );
init();
}
DSNLEXER::~DSNLEXER()
{
if( iOwnReaders )
{
// delete the LINE_READERs from the stack, since I own them.
for( READER_STACK::iterator it = readerStack.begin(); it!=readerStack.end(); ++it )
delete *it;
}
}
void DSNLEXER::SetSpecctraMode( bool aMode )
{
specctraMode = aMode;
if( aMode )
{
// specctra mode defaults, some of which can still be changed in this mode.
space_in_quoted_tokens = true;
}
else
{
space_in_quoted_tokens = false;
stringDelimiter = '"';
}
}
void DSNLEXER::InitParserState()
{
curTok = DSN_NONE;
prevTok = DSN_NONE;
commentsAreTokens = false;
curOffset = 0;
}
bool DSNLEXER::SyncLineReaderWith( DSNLEXER& aLexer )
{
// Synchronize the pointers handling the data read by the LINE_READER
// only if aLexer shares the same LINE_READER, because only in this case
// the char buffer is be common
if( reader != aLexer.reader )
return false;
// We can synchronize the pointers which handle the data currently read
start = aLexer.start;
next = aLexer.next;
limit = aLexer.limit;
// Sync these parameters is not mandatory, but could help
// for instance in debug
curText = aLexer.curText;
curOffset = aLexer.curOffset;
return true;
}
void DSNLEXER::PushReader( LINE_READER* aLineReader )
{
readerStack.push_back( aLineReader );
reader = aLineReader;
start = (const char*) (*reader);
// force a new readLine() as first thing.
limit = start;
next = start;
}
LINE_READER* DSNLEXER::PopReader()
{
LINE_READER* ret = nullptr;
if( readerStack.size() )
{
ret = reader;
readerStack.pop_back();
if( readerStack.size() )
{
reader = readerStack.back();
start = reader->Line();
// force a new readLine() as first thing.
limit = start;
next = start;
}
else
{
reader = nullptr;
start = dummy;
limit = dummy;
}
}
return ret;
}
int DSNLEXER::findToken( const std::string& tok ) const
{
if( keywordsLookup != nullptr )
{
KEYWORD_MAP::const_iterator it = keywordsLookup->find( tok.c_str() );
if( it != keywordsLookup->end() )
return it->second;
}
return DSN_SYMBOL; // not a keyword, some arbitrary symbol.
}
const char* DSNLEXER::Syntax( int aTok )
{
const char* ret;
switch( aTok )
{
case DSN_NONE:
ret = "NONE";
break;
case DSN_STRING_QUOTE:
ret = "string_quote"; // a special DSN syntax token, see specctra spec.
break;
case DSN_QUOTE_DEF:
ret = "quoted text delimiter";
break;
case DSN_DASH:
ret = "-";
break;
case DSN_SYMBOL:
ret = "symbol";
break;
case DSN_NUMBER:
ret = "number";
break;
case DSN_RIGHT:
ret = ")";
break;
case DSN_LEFT:
ret = "(";
break;
case DSN_STRING:
ret = "quoted string";
break;
case DSN_EOF:
ret = "end of input";
break;
default:
ret = "???";
}
return ret;
}
const char* DSNLEXER::GetTokenText( int aTok ) const
{
const char* ret;
if( aTok < 0 )
{
return Syntax( aTok );
}
else if( (unsigned) aTok < keywordCount )
{
ret = keywords[aTok].name;
}
else
ret = "token too big";
return ret;
}
wxString DSNLEXER::GetTokenString( int aTok ) const
{
wxString ret;
ret << wxT("'") << wxString::FromUTF8( GetTokenText(aTok) ) << wxT("'");
return ret;
}
bool DSNLEXER::IsSymbol( int aTok )
{
// This is static and not inline to reduce code space.
// if aTok is >= 0, then it is a coincidental match to a keyword.
return aTok == DSN_SYMBOL || aTok == DSN_STRING || aTok >= 0;
}
void DSNLEXER::Expecting( int aTok ) const
{
wxString errText = wxString::Format(
_( "Expecting %s" ), GetTokenString( aTok ) );
THROW_PARSE_ERROR( errText, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
void DSNLEXER::Expecting( const char* text ) const
{
wxString errText = wxString::Format(
_( "Expecting '%s'" ), wxString::FromUTF8( text ) );
THROW_PARSE_ERROR( errText, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
void DSNLEXER::Unexpected( int aTok ) const
{
wxString errText = wxString::Format(
_( "Unexpected %s" ), GetTokenString( aTok ) );
THROW_PARSE_ERROR( errText, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
void DSNLEXER::Duplicate( int aTok )
{
wxString errText = wxString::Format(
_("%s is a duplicate"), GetTokenString( aTok ).GetData() );
THROW_PARSE_ERROR( errText, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
void DSNLEXER::Unexpected( const char* text ) const
{
wxString errText = wxString::Format(
_( "Unexpected '%s'" ), wxString::FromUTF8( text ) );
THROW_PARSE_ERROR( errText, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
void DSNLEXER::NeedLEFT()
{
int tok = NextTok();
if( tok != DSN_LEFT )
Expecting( DSN_LEFT );
}
void DSNLEXER::NeedRIGHT()
{
int tok = NextTok();
if( tok != DSN_RIGHT )
Expecting( DSN_RIGHT );
}
int DSNLEXER::NeedSYMBOL()
{
int tok = NextTok();
if( !IsSymbol( tok ) )
Expecting( DSN_SYMBOL );
return tok;
}
int DSNLEXER::NeedSYMBOLorNUMBER()
{
int tok = NextTok();
if( !IsSymbol( tok ) && tok!=DSN_NUMBER )
Expecting( "a symbol or number" );
return tok;
}
int DSNLEXER::NeedNUMBER( const char* aExpectation )
{
int tok = NextTok();
if( tok != DSN_NUMBER )
{
wxString errText = wxString::Format( _( "need a number for '%s'" ),
wxString::FromUTF8( aExpectation ).GetData() );
THROW_PARSE_ERROR( errText, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
return tok;
}
/**
* Test for whitespace.
*
* Our whitespace, by our definition, is a subset of ASCII, i.e. no bytes with MSB on can be
* considered whitespace, since they are likely part of a multibyte UTF8 character.
*/
static bool isSpace( char cc )
{
// cc is signed, so it is often negative.
// Treat negative as large positive to exclude rapidly.
if( (unsigned char) cc <= ' ' )
{
switch( (unsigned char) cc )
{
case ' ':
case '\n':
case '\r':
case '\t':
case '\0': // PCAD s-expression files have this.
return true;
}
}
return false;
}
inline bool isDigit( char cc )
{
return '0' <= cc && cc <= '9';
}
///< @return true if @a cc is an s-expression separator character.
inline bool isSep( char cc )
{
return isSpace( cc ) || cc=='(' || cc==')';
}
/**
* Return true if the next sequence of text is a number:
* either an integer, fixed point, or float with exponent. Stops scanning
* at the first non-number character, even if it is not whitespace.
*
* @param cp is the start of the current token.
* @param limit is the end of the current token.
* @return true if input token is a number, else false.
*/
static bool isNumber( const char* cp, const char* limit )
{
// regex for a float: "^[-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?" i.e. any number,
// code traversal manually here:
bool sawNumber = false;
if( cp < limit && ( *cp=='-' || *cp=='+' ) )
++cp;
while( cp < limit && isDigit( *cp ) )
{
++cp;
sawNumber = true;
}
if( cp < limit && *cp == '.' )
{
++cp;
while( cp < limit && isDigit( *cp ) )
{
++cp;
sawNumber = true;
}
}
if( sawNumber )
{
if( cp < limit && ( *cp=='E' || *cp=='e' ) )
{
++cp;
sawNumber = false; // exponent mandates at least one digit thereafter.
if( cp < limit && ( *cp=='-' || *cp=='+' ) )
++cp;
while( cp < limit && isDigit( *cp ) )
{
++cp;
sawNumber = true;
}
}
}
return sawNumber && cp==limit;
}
int DSNLEXER::NextTok()
{
const char* cur = next;
const char* head = cur;
prevTok = curTok;
if( curTok == DSN_EOF )
goto exit;
if( cur >= limit )
{
L_read:
// blank lines are returned as "\n" and will have a len of 1.
// EOF will have a len of 0 and so is detectable.
int len = readLine();
if( len == 0 )
{
cur = start; // after readLine(), since start can change, set cur offset to start
curTok = DSN_EOF;
goto exit;
}
cur = start; // after readLine() since start can change.
// skip leading whitespace
while( cur < limit && isSpace( *cur ) )
++cur;
// If the first non-blank character is #, this line is a comment.
// Comments cannot follow any other token on the same line.
if( cur<limit && *cur=='#' )
{
if( commentsAreTokens )
{
// Grab the entire current line [excluding end of line char(s)] as the
// current token. The '#' character may not be at offset zero.
while( limit[-1] == '\n' || limit[-1] == '\r' )
--limit;
curText.clear();
curText.append( start, limit );
cur = start; // ensure a good curOffset below
curTok = DSN_COMMENT;
head = limit; // do a readLine() on next call in here.
goto exit;
}
else
{
goto L_read;
}
}
}
else
{
// skip leading whitespace
while( cur < limit && isSpace( *cur ) )
++cur;
}
if( cur >= limit )
goto L_read;
if( *cur == '(' )
{
curText = *cur;
curTok = DSN_LEFT;
head = cur+1;
goto exit;
}
if( *cur == ')' )
{
curText = *cur;
curTok = DSN_RIGHT;
head = cur+1;
goto exit;
}
// Non-specctraMode, understands and deciphers escaped \, \r, \n, and \".
// Strips off leading and trailing double quotes
if( !specctraMode )
{
// a quoted string, will return DSN_STRING
if( *cur == stringDelimiter )
{
// copy the token, character by character so we can remove doubled up quotes.
curText.clear();
++cur; // skip over the leading delimiter, which is always " in non-specctraMode
head = cur;
while( head<limit )
{
// ESCAPE SEQUENCES:
if( *head =='\\' )
{
char tbuf[8];
char c;
int i;
if( ++head >= limit )
break; // throw exception at L_unterminated
switch( *head++ )
{
case '"':
case '\\': c = head[-1]; break;
case 'a': c = '\x07'; break;
case 'b': c = '\x08'; break;
case 'f': c = '\x0c'; break;
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\x09'; break;
case 'v': c = '\x0b'; break;
case 'x': // 1 or 2 byte hex escape sequence
for( i = 0; i < 2; ++i )
{
if( !isxdigit( head[i] ) )
break;
tbuf[i] = head[i];
}
tbuf[i] = '\0';
if( i > 0 )
c = (char) strtoul( tbuf, nullptr, 16 );
else
c = 'x'; // a goofed hex escape sequence, interpret as 'x'
head += i;
break;
default: // 1-3 byte octal escape sequence
--head;
for( i=0; i<3; ++i )
{
if( head[i] < '0' || head[i] > '7' )
break;
tbuf[i] = head[i];
}
tbuf[i] = '\0';
if( i > 0 )
c = (char) strtoul( tbuf, nullptr, 8 );
else
c = '\\'; // a goofed octal escape sequence, interpret as '\'
head += i;
break;
}
curText += c;
}
else if( *head == '"' ) // end of the non-specctraMode DSN_STRING
{
curTok = DSN_STRING;
++head; // omit this trailing double quote
goto exit;
}
else
curText += *head++;
} // while
// L_unterminated:
wxString errtxt( _( "Un-terminated delimited string" ) );
THROW_PARSE_ERROR( errtxt, CurSource(), CurLine(), CurLineNumber(),
cur - start + curText.length() );
}
}
else // is specctraMode, tests in this block should not occur in KiCad mode.
{
/* get the dash out of a <pin_reference> which is embedded for example
like: U2-14 or "U2"-"14"
This is detectable by a non-space immediately preceding the dash.
*/
if( *cur == '-' && cur>start && !isSpace( cur[-1] ) )
{
curText = '-';
curTok = DSN_DASH;
head = cur+1;
goto exit;
}
// switching the string_quote character
if( prevTok == DSN_STRING_QUOTE )
{
static const wxString errtxt( _("String delimiter must be a single character of "
"', \", or $") );
char cc = *cur;
switch( cc )
{
case '\'':
case '$':
case '"':
break;
default:
THROW_PARSE_ERROR( errtxt, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
curText = cc;
head = cur+1;
if( head<limit && !isSep( *head ) )
{
THROW_PARSE_ERROR( errtxt, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
curTok = DSN_QUOTE_DEF;
goto exit;
}
// specctraMode DSN_STRING
if( *cur == stringDelimiter )
{
++cur; // skip over the leading delimiter: ",', or $
head = cur;
while( head<limit && !isStringTerminator( *head ) )
++head;
if( head >= limit )
{
wxString errtxt( _( "Un-terminated delimited string" ) );
THROW_PARSE_ERROR( errtxt, CurSource(), CurLine(), CurLineNumber(), CurOffset() );
}
curText.clear();
curText.append( cur, head );
++head; // skip over the trailing delimiter
curTok = DSN_STRING;
goto exit;
}
} // specctraMode
// non-quoted token, read it into curText.
curText.clear();
head = cur;
while( head<limit && !isSep( *head ) )
curText += *head++;
if( isNumber( curText.c_str(), curText.c_str() + curText.size() ) )
{
curTok = DSN_NUMBER;
goto exit;
}
if( specctraMode && curText == "string_quote" )
{
curTok = DSN_STRING_QUOTE;
goto exit;
}
curTok = findToken( curText );
exit: // single point of exit, no returns elsewhere please.
curOffset = cur - start;
next = head;
return curTok;
}
wxArrayString* DSNLEXER::ReadCommentLines()
{
wxArrayString* ret = nullptr;
bool cmt_setting = SetCommentsAreTokens( true );
int tok = NextTok();
if( tok == DSN_COMMENT )
{
ret = new wxArrayString();
do
{
ret->Add( FromUTF8() );
}
while( ( tok = NextTok() ) == DSN_COMMENT );
}
SetCommentsAreTokens( cmt_setting );
return ret;
}
double DSNLEXER::parseDouble()
{
#if ( defined( __GNUC__ ) && __GNUC__ < 11 ) || ( defined( __clang__ ) && __clang_major__ < 13 )
// GCC older than 11 "supports" C++17 without supporting the C++17 std::from_chars for doubles
// clang is similar
char* tmp;
errno = 0;
double fval = strtod( CurText(), &tmp );
if( errno )
{
wxString error;
error.Printf( _( "Invalid floating point number in\nfile: '%s'\nline: %d\noffset: %d" ),
CurSource(), CurLineNumber(), CurOffset() );
THROW_IO_ERROR( error );
}
if( CurText() == tmp )
{
wxString error;
error.Printf( _( "Missing floating point number in\nfile: '%s'\nline: %d\noffset: %d" ),
CurSource(), CurLineNumber(), CurOffset() );
THROW_IO_ERROR( error );
}
return fval;
#else
// Use std::from_chars which is designed to be locale independent and performance oriented for data interchange
const std::string& str = CurStr();
// Offset any leading whitespace, this is one thing from_chars does not handle
size_t woff = 0;
while( std::isspace( str[woff] ) && woff < str.length() )
{
woff++;
}
double dval{};
std::from_chars_result res =
std::from_chars( str.data() + woff, str.data() + str.size(), dval );
if( res.ec != std::errc() )
{
THROW_PARSE_ERROR( _( "Invalid floating point number" ), CurSource(), CurLine(),
CurLineNumber(), CurOffset() );
}
return dval;
#endif
}