// SPDX-License-Identifier: MIT
#include "Parser.hh"
#include <cctype>
#include <exception>
#include <filesystem>
#include <iostream>
#include <stdexcept>
#include <system_error>
#include "Cursor.hh"
using namespace std;
namespace fedem
{
namespace parser
{
Parser::Parser( ) = default;
Parser::~Parser( ) = default;
// Resolve `filename` to a canonical path and point `cursor` at a freshly
// opened Cursor for it. Shared by parse() and openStream() — both need
// exactly this and nothing more; what happens after (run start(), or
// just stop and leave the cursor for the caller to drive) is where they
// diverge. `caller` names the public entry point for the clog tag
// ("[Parser::parse]" / "[Parser::openStream]") — kept distinct rather
// than a single generic tag so a diagnostic still points at which one
// was actually called; ParserTest.ParseMissingFile pins parse()'s.
bool Parser::openCursorFor( string const& filename, string const& caller )
{
filesystem::path absolutePath;
try
{
absolutePath = filesystem::canonical( filename );
cursor = make_shared< Cursor >( absolutePath.string( ) );
return true;
}
catch( filesystem::filesystem_error const& e )
{
// Infrastructure error — file not found / permission denied.
// Use e.what() which contains the full descriptive message.
// Not a grammar error, so goes to clog (not the error collector).
clog << "[Parser::" << caller << "] filesystem error: cannot make canonical path: "
<< e.code( ).message( ) << " [" << filename << "]\n";
}
catch( ios_base::failure const& e )
{
if( e.code( ).category( ) == iostream_category( ) && e.code( ).value( ) == 1 )
clog << "[Parser::" << caller << "] file not found '" << absolutePath << "'\n";
else
clog << "[Parser::" << caller << "] I/O failure: " << e.code( ).message( ) << "\n";
}
catch( exception const& ex )
{
clog << "[Parser::" << caller << "] " << ex.what( ) << '\n';
}
catch( ... )
{
clog << "[Parser::" << caller << "] undefined error";
}
return false;
}
bool Parser::openStream( string filename )
{
clearErrors( );
if( !cursor )
openScopes_ = 0U;
if( cursor )
{
cursor->store( );
returnPoints.push( cursor );
}
// Deliberately stops here: no start(), no teardown. See the doc
// comment in Parser.hh for why parse() cannot be reused for this.
return openCursorFor( filename, "openStream" );
}
bool Parser::parse( string filename )
{
bool result = false;
clearErrors( );
// Only the OUTERMOST parse resets the scope depth. A nested parse()
// is an include: the including grammar legitimately has scopes open
// around it, and clearing the count here would hide a truncated
// include behind its parent's still-open braces.
if( !cursor )
openScopes_ = 0U;
if( cursor )
{
cursor->store( );
returnPoints.push( cursor );
}
result = openCursorFor( filename, "parse" );
if( result )
{
result = false;
try
{
result = start( );
if( !result )
{
// start() returned false — grammar signalled failure without exit()
recordError( ParseError::Kind::Internal,
"unexpected file parsing result" );
}
else if( !cursor->isValid( ) || !returnPoints.empty( ) )
{
// Cursor is invalid (EOF reached) or there are nested parse contexts.
// For a full-file parser (declaration list) this is the normal
// successful ending — the EOF exception fires and sets result=true
// via the ios_base::failure handler below. If we reach here, the
// stream ended cleanly without throwing, which is also acceptable.
// Demote to Warning so callers can distinguish from real failures.
if( !returnPoints.empty( ) )
{
recordError( ParseError::Kind::Warning,
"parser returned with unclosed cursor stack" );
result = false;
}
// else: cursor simply reached EOF cleanly — leave result=true.
}
}
catch( Exited const& )
{
// A fatal grammar error was recorded via recordError() before
// exit() was called. Clear the cursor stack and return false.
while( !returnPoints.empty( ) )
returnPoints.pop( );
result = false;
}
catch( ios_base::failure const& e )
{
if( e.code( ).category( ) != iostream_category( ) || e.code( ).value( ) != 1 )
{
recordError( ParseError::Kind::Internal,
"I/O failure: " + e.code( ).message( ) );
}
else if( returnPoints.empty( ) )
{
// End of input. Whether that is success depends on what the
// grammar still had open — see enterScope() in the header.
if( openScopes_ > 0U )
{
recordError( ParseError::Kind::Syntax,
"unexpected end of input: "
+ std::to_string( openScopes_ )
+ " unterminated scope(s)" );
result = false;
}
else
{
result = true; // normal EOF
}
}
}
catch( runtime_error& error )
{
recordError( ParseError::Kind::Internal, error.what( ) );
}
catch( int& code )
{
recordError( ParseError::Kind::Internal,
"error code " + to_string( code ) );
}
catch( ... )
{
recordError( ParseError::Kind::Internal, "undefined error" );
}
}
if( returnPoints.empty( ) )
cursor.reset( );
else
{
cursor = returnPoints.top( );
returnPoints.pop( );
cursor->restore( );
result = true;
}
return result;
}
// ── open-scope tracking ────────────────────────────────────────────
// See Parser.hh for why this is not an RAII guard.
void Parser::enterScope( ) noexcept
{
++openScopes_;
}
void Parser::leaveScope( ) noexcept
{
if( openScopes_ > 0U )
--openScopes_;
}
unsigned int Parser::openScopeDepth( ) const noexcept
{
return openScopes_;
}
void Parser::recordError(
ParseError::Kind kind,
string const& message,
unsigned long int line,
unsigned long int col )
{
ParseError e;
e.kind = kind;
e.message = message;
e.filename = cursor ? cursor->getFilename( ) : "";
e.line = ( line != 0 ) ? line
: ( cursor ? cursor->getNextLineNumber( ) : 0 );
e.col = ( col != 0 ) ? col
: ( cursor ? cursor->getNextColumnNumber( ) : 0 );
errors_.push_back( std::move( e ) );
}
void Parser::writeTraceMessage( )
{
clog << " in file \"" << cursor->getFilename( )
<< "\" at line " << cursor->getLineNumber( )
<< " and column " << cursor->getColumnNumber( ) << endl;
unsigned int count = 0;
unsigned long int line = 0;
Cursor::stream_type::char_type read = cursor->get( );
while( ( line < 3U ) && cursor->isValid( ) )
{
if( ( count > 120U ) || ( read == '\n' ) )
{
++line;
count = 0;
}
clog << ( isprint( static_cast< unsigned char >( read ) ) || read == '\n' ? read : ' ' );
read = cursor->get( );
++count;
}
clog << endl;
}
bool Parser::skipWhiteSpaces( )
{
bool const whiteSpace = isspace( static_cast< unsigned char >( cursor->safePeek( ) ) );
if( whiteSpace )
{
try
{
do
{
cursor->get( );
} while( isspace( static_cast< unsigned char >( cursor->peek( ) ) ) );
}
catch( ... )
{
// EOF or stream failure — normal
}
}
return whiteSpace;
}
bool Parser::skipComments( )
{
return false;
}
bool Parser::skipCommentsBlock( )
{
bool result = false;
bool remark = false;
do
{
bool whiteSpace = skipWhiteSpaces( );
remark = skipComments( );
result = result || whiteSpace || remark;
} while( remark );
return result;
}
bool Parser::extractToken( string const& token )
{
bool matched = false;
if( cursor->isValid( ) && !token.empty( ) )
{
Cursor save = storeCursor( );
auto pos = token.cbegin( );
while( pos != token.cend( ) && *pos == cursor->safePeek( ) )
{
cursor->get( );
++pos;
}
matched = ( pos == token.cend( ) );
if( !matched )
restoreCursor( save );
}
return matched;
}
bool Parser::extractCharacter( string::value_type const token )
{
auto read = cursor->safePeek( );
bool matched = token == read;
if( matched )
cursor->get( );
return matched;
}
void Parser::exit( int status )
{
Exited exitException( status );
throw exitException;
}
void Parser::pushCursor( string const& sourceName,
shared_ptr< Cursor::stream_type > stream )
{
if( cursor )
{
cursor->store( );
returnPoints.push( cursor );
}
cursor = make_shared< Cursor >( sourceName, stream );
}
Cursor const& Parser::storeCursor( )
{
cursor->store( );
return *cursor;
}
void Parser::restoreCursor( Cursor const& crs )
{
*cursor = crs;
cursor->restore( );
}
Parser::Exited::Exited( int code )
{
exitCode = code;
}
int Parser::Exited::getExitCode( ) const
{
return exitCode;
}
} // end namespace parser
} // end namespace fedem