#include "externals.hh"

#include "common.hh"
#include "c-project.hh"
#include "c-opast.hh"
#include "c-ops.hh"
#include "c-opcomp.hh"
#include "c-syncoverrides.hh"

#include <ebcl/Algorithms.hh>
#include <ebcl/Files.hh>
#include <ebcl/SRDParser.hh>
#include <ebcl/SRDText.hh>

using namespace ebcl;
using namespace opast;


#define M_LOGSTR_( S , L ) \
	logger( [](){ return T_StringBuilder{ S }; } , L )



/*= T_Parser =================================================================*/

namespace {

struct T_ParserImpl_
{
	enum class E_InstrType {
		CALL ,
		CLEAR ,
		COMPUTE ,
		FRAMEBUFFER ,
		FULLSCREEN ,
		IF ,
		INPUT ,
		LOCALS ,
		MAINOUT ,
		ODBG ,
		OVERRIDES ,
		PIPELINE ,
		PROFILE ,
		PROGRAM ,
		SAMPLER ,
		SET ,
		TEXTURE ,
		UNIFORMS_FLT ,
		UNIFORMS_INT ,
		USE_FRAMEBUFFER ,
		USE_PIPELINE ,
		USE_PROGRAM ,
		USE_TEXTURE ,
		IMAGE ,
		VIEWPORT ,
	};

	const T_KeyValueTable< T_String , E_InstrType > instrMap{ ([]() {
		T_KeyValueTable< T_String , E_InstrType > temp{ 64 , 128 , 64 };
		const auto add{ [&temp]( char const* name , E_InstrType it ) {
			temp.add( T_String::Pooled( name ) , it );
		} };

		add( "call" , E_InstrType::CALL );
		add( "clear" , E_InstrType::CLEAR );
		add( "compute" , E_InstrType::COMPUTE );
		add( "framebuffer" , E_InstrType::FRAMEBUFFER );
		add( "fullscreen" , E_InstrType::FULLSCREEN );
		add( "if" , E_InstrType::IF );
		add( "input" , E_InstrType::INPUT );
		add( "locals" , E_InstrType::LOCALS );
		add( "main-output" , E_InstrType::MAINOUT );
		add( "odbg" , E_InstrType::ODBG );
		add( "pipeline" , E_InstrType::PIPELINE );
		add( "profiling" , E_InstrType::PROFILE );
		add( "program" , E_InstrType::PROGRAM );
		add( "sampler" , E_InstrType::SAMPLER );
		add( "set" , E_InstrType::SET );
		add( "texture" , E_InstrType::TEXTURE );
		add( "ui-overrides" , E_InstrType::OVERRIDES );
		add( "uniforms" , E_InstrType::UNIFORMS_FLT );
		add( "uniforms-i" , E_InstrType::UNIFORMS_INT );
		add( "use-framebuffer" , E_InstrType::USE_FRAMEBUFFER );
		add( "use-pipeline" , E_InstrType::USE_PIPELINE );
		add( "use-program" , E_InstrType::USE_PROGRAM );
		add( "use-texture" , E_InstrType::USE_TEXTURE );
		add( "image" , E_InstrType::IMAGE );
		add( "viewport" , E_InstrType::VIEWPORT );

		return temp;
	})( ) };

	const T_KeyValueTable< T_String , T_UnaryOperatorNode::E_Operator > unaryOpMap{ ([]() {
		T_KeyValueTable< T_String , T_UnaryOperatorNode::E_Operator > temp{ 32 , 32 , 32 };
		const auto add{ [&temp]( char const* name ,
				const T_UnaryOperatorNode::E_Operator it ) {
			temp.add( T_String::Pooled( name ) , it );
		} };

		add( "neg" , T_UnaryOperatorNode::NEG );
		add( "inv" , T_UnaryOperatorNode::INV );
		add( "not" , T_UnaryOperatorNode::NOT );
		add( "sin" , T_UnaryOperatorNode::SIN );
		add( "cos" , T_UnaryOperatorNode::COS );
		add( "tan" , T_UnaryOperatorNode::TAN );
		add( "sqrt" , T_UnaryOperatorNode::SQRT );
		add( "exp" , T_UnaryOperatorNode::EXP );
		add( "ln" , T_UnaryOperatorNode::LN );

		return temp;
	})( ) };

	const T_KeyValueTable< T_String , E_TexType > texTypeMap{ ([]() {
		T_KeyValueTable< T_String , E_TexType > temp{ 16 , 16 , 16 };
		const auto add{ [&temp]( char const* name ,
				const E_TexType it ) {
			temp.add( T_String::Pooled( name ) , it );
		} };

		add( "rgba-nu8" , E_TexType::RGBA8 );
		add( "rgba-f16" , E_TexType::RGBA16F );
		add( "rgb-nu8" , E_TexType::RGB8 );
		add( "rgb-f16" , E_TexType::RGB16F );
		add( "r-nu8" , E_TexType::R8 );
		add( "r-f16" , E_TexType::R16F );

		return temp;
	})( ) };

	const T_KeyValueTable< T_String , T_BinaryOperatorNode::E_Operator > binOpMap{ ([]() {
		T_KeyValueTable< T_String , T_BinaryOperatorNode::E_Operator > temp{ 32 , 32 , 32 };
		const auto add{ [&temp]( char const* name ,
				const T_BinaryOperatorNode::E_Operator it ) {
			temp.add( T_String::Pooled( name ) , it );
		} };

		add( "add" , T_BinaryOperatorNode::ADD );
		add( "sub" , T_BinaryOperatorNode::SUB );
		add( "mul" , T_BinaryOperatorNode::MUL );
		add( "div" , T_BinaryOperatorNode::DIV );
		add( "pow" , T_BinaryOperatorNode::POW );
		add( "cmp-eq" , T_BinaryOperatorNode::CMP_EQ );
		add( "cmp-ne" , T_BinaryOperatorNode::CMP_NE );
		add( "cmp-gt" , T_BinaryOperatorNode::CMP_GT );
		add( "cmp-ge" , T_BinaryOperatorNode::CMP_GE );
		add( "cmp-lt" , T_BinaryOperatorNode::CMP_LT );
		add( "cmp-le" , T_BinaryOperatorNode::CMP_LE );

		return temp;
	})( ) };

	const T_KeyValueTable< T_String , E_ODbgMode > odbgModes{ ([]() {
		T_KeyValueTable< T_String , E_ODbgMode > temp{ 32 , 32 , 32 };
		const auto add{ [&temp]( char const* name ,
				const E_ODbgMode it ) {
			temp.add( T_String::Pooled( name ) , it );
		} };

		add( "hdr" , E_ODbgMode::HDR );
		add( "ldr" , E_ODbgMode::LDR );
		add( "ldr-alpha" , E_ODbgMode::LDR_ALPHA );
		add( "depth" , E_ODbgMode::DEPTH );

		assert( temp.size( ) == uint32_t( E_ODbgMode::__COUNT__ ) );

		return temp;
	})( ) };

	// ---------------------------------------------------------------------

	T_OwnPtr< T_OpsParserOutput >& output;
	T_Array< T_SRDError >& errors;
	F_OPLogger& logger;
	T_MultiArray< uint32_t > calls;
	T_Array< T_InstrRestriction > callInfo;
	T_SRDParserConfig ovParserConfig;
	T_SRDParser ovParser;

	T_FSPath curFile;
	T_FSPath baseDir;
	T_AutoArray< T_FSPath , 32 > inclStack;

	T_Visitor< A_Node > visitor{ opast::ASTVisitorBrowser };
	T_Visitor< uint32_t , uint32_t > callGraphVisitor{
			[this]( uint32_t v , uint32_t child ) -> T_Optional< uint32_t > {
				const uint32_t nc( calls.sizeOf( v ) );
				if ( child < nc ) {
					return calls.get( v , child );
				}
				return {};
			} };
	T_SyncOverrideVisitor ovVisitor;

	T_ParserImpl_( T_Array< T_SRDError >* errors ,
			T_OwnPtr< T_OpsParserOutput >* root ,
			F_OPLogger* logger ) noexcept;

	// ---------------------------------------------------------------------

	void main( T_FSPath const& file ,
			T_SRDList const& list ) noexcept;

	// ---------------------------------------------------------------------

    private:
	bool checkRequiredBlocks( T_SRDList const& list ) noexcept;
	bool checkCalls( ) noexcept;
	bool checkInstructionRestrictions( ) noexcept;
	bool collectGlobalTypes( ) noexcept;
	bool checkLocalVariables( ) noexcept;
	bool checkArgumentTypes( ) noexcept;

	bool checkIdentifiers( ) noexcept;
	void checkIdentifier(
			T_String const& id ,
			T_SRDLocation const& location ,
			const uint32_t funcIndex ,
			const E_DataType expected ) noexcept;
	void ciInput( uint32_t funcIndex ,
			A_Node& input ) noexcept;
	void ciIdentifier( uint32_t funcIndex ,
			A_Node& input ) noexcept;

	E_DataType getTypeOf( T_String const& name ,
			A_FuncNode const* context ) const noexcept;

	// ---------------------------------------------------------------------

	bool parseTopLevel( T_SRDList const& list ) noexcept;
	void parseTLList( T_SRDList const& list ) noexcept;
	void parseTLListItem( T_SRDList const& funcList ) noexcept;

	void handleInclude( T_SRDToken const& tFileName ) noexcept;

	void parseFunction( T_SRDList const& funcList ) noexcept;
	void parseFunctionArguments(
			T_FuncNode& function ,
			T_SRDToken const& argsToken ) noexcept;

	void parseInstructions(
			T_InstrListNode& instructions ,
			T_SRDList const& input ,
			uint32_t start ) noexcept;
	P_InstrListNode parseBlock(
			A_Node& parent ,
			T_SRDToken const& block ) noexcept;

	// ---------------------------------------------------------------------

#define M_DPARSER_( NAME ) \
	void parse##NAME##Instruction( \
			T_InstrListNode& instructions , \
			T_SRDList const& input ) noexcept

	M_DPARSER_( Call );
	M_DPARSER_( Clear );
	M_DPARSER_( Compute );

	M_DPARSER_( Framebuffer );
	bool parseFramebufferEntry(
			T_FramebufferInstrNode& instruction ,
			T_SRDToken const& entry ) noexcept;

	M_DPARSER_( If );
	M_DPARSER_( Input );
	M_DPARSER_( Locals );
	M_DPARSER_( ODebug );
	M_DPARSER_( Overrides );
	M_DPARSER_( Pipeline );
	M_DPARSER_( Profile );
	M_DPARSER_( Program );

	M_DPARSER_( Sampler );
	void parseSamplerSampling(
			T_SamplerInstrNode& instruction ,
			T_SRDList const& input );
	void parseSamplerMipmaps(
			T_SamplerInstrNode& instruction ,
			T_SRDList const& input );
	void parseSamplerWrapping(
			T_SamplerInstrNode& instruction ,
			T_SRDList const& input );
	void parseSamplerLOD(
			T_SamplerInstrNode& instruction ,
			T_SRDList const& input );

	M_DPARSER_( Set );
	M_DPARSER_( Texture );

	M_DPARSER_( Uniforms );
	M_DPARSER_( UniformsI );
	void parseUniformsCommon(
			T_InstrListNode& instructions ,
			T_SRDList const& input ,
			bool integers ) noexcept;

	M_DPARSER_( UseFramebuffer );
	M_DPARSER_( UsePipeline );
	M_DPARSER_( UseProgram );
	void parseUseCommon(
			T_InstrListNode& instructions ,
			T_SRDList const& input ,
			T_UseInstrNode::E_Type type ) noexcept;
	M_DPARSER_( UseTexture );
	M_DPARSER_( Image );

	M_DPARSER_( Viewport );

#undef M_DPARSER_

	// ---------------------------------------------------------------------

	P_ExpressionNode parseExpression(
			A_Node& parent ,
			T_SRDToken const& token ) noexcept;
	P_ExpressionNode parseOperation(
			A_Node& parent ,
			T_SRDList const& opList ) noexcept;
	P_ExpressionNode parseBinOp(
			A_Node& parent ,
			T_SRDList const& opList ,
			T_BinaryOperatorNode::E_Operator op ) noexcept;
	P_ExpressionNode parseUnaryOp(
			A_Node& parent ,
			T_SRDList const& opList ,
			T_UnaryOperatorNode::E_Operator op ) noexcept;
};

/*----------------------------------------------------------------------------*/

inline T_ParserImpl_::T_ParserImpl_(
		T_Array< T_SRDError >* const errors ,
		T_OwnPtr< T_OpsParserOutput >* const output ,
		F_OPLogger* logger ) noexcept
	: output( *output ) , errors( *errors ) , logger{ *logger } ,
		ovParserConfig( sov::GetParserConfig( ) ) ,
		ovParser( ovParserConfig )
{ }

void T_ParserImpl_::main(
		T_FSPath const& file ,
		T_SRDList const& input ) noexcept
{
	assert( file.isAbsolute( ) );
	curFile = file.canonical( );
	baseDir = file.parent( );
	output->files.add( file );
	parseTopLevel( input )
		&& checkRequiredBlocks( input )
		&& checkCalls( )
		&& checkInstructionRestrictions( )
		&& checkLocalVariables( )
		&& collectGlobalTypes( )
		&& checkArgumentTypes( )
		&& checkIdentifiers( );
}

/*----------------------------------------------------------------------------*/

/* Check that both the initialisation and frame rendering blocks have been
 * defined.
 */
bool T_ParserImpl_::checkRequiredBlocks(
		T_SRDList const& input ) noexcept
{
	M_LOGSTR_( "... Checking for required blocks" , 2 );
	const T_SRDLocation missingErrLoc( ([&input]() {
		if ( input.size( ) != 0 ) {
			return T_SRDLocation( input[ 0 ].location( ).source( ) , 1 , 1 );
		}
		return T_SRDLocation{};
	})( ));
	if ( !output->root.hasInit( ) ) {
		errors.addNew( "no initialisation block" , missingErrLoc );
	}
	if ( !output->root.hasFrame( ) ) {
		errors.addNew( "no frame rendering block" , missingErrLoc );
	}
	return errors.empty( );
}

/* Gather function calls into a call graph and check that function calls
 * provide the correct quantity of arguments.
 */
bool T_ParserImpl_::checkCalls( ) noexcept
{
	M_LOGSTR_( "... Gathering/checking calls" , 2 );
	calls.clear( );
	uint32_t cfi;
	for ( cfi = 0 ; cfi < output->root.nFunctions( ) ; cfi ++ ) {
		calls.next( );
		visitor.visit( output->root.function( cfi ) , [&]( A_Node& node , bool exit ) -> bool {
			if ( exit || dynamic_cast< A_ExpressionNode* >( &node ) ) {
				return false;
			}
			if ( node.type( ) != A_Node::OP_CALL ) {
				return true;
			}

			auto& call( (T_CallInstrNode&) node );
			const auto callee( output->root.functionIndex( call.id( ) ) );
			if ( callee >= 0 ) {
				if ( !calls.contains( cfi , callee ) ) {
					calls.add( (uint32_t) callee );
				}

				// Check argument count while we're at it
				auto& fn( (T_FuncNode&) output->root.function( callee ) );
				if ( fn.arguments( ) != call.size( ) ) {
					T_StringBuilder sb;
					sb << "function expects " << fn.arguments( )
						<< " argument" << ( fn.arguments( ) == 1 ? "" : "s" )
						<< ", " << call.size( )
						<< " argument" << ( call.size( ) == 1 ? "" : "s" )
						<< " provided";
					errors.addNew( std::move( sb ) , call.location( ) );
				}
			} else {
				errors.addNew( "unknown function" , call.idLocation( ) );
			}
			return false;
		} );
	}
	return errors.empty( );
}

/* Go through the call graph, determine whether functions are called from the
 * initialisation block, the frame rendering block, or both, and then enforce
 * restrictions on instructions. Also, remove functions that are never called.
 */
bool T_ParserImpl_::checkInstructionRestrictions( ) noexcept
{
	M_LOGSTR_( "... Checking instruction restrictions" , 2 );
	callInfo.clear( );
	callInfo.resize( calls.size( ) );

	callGraphVisitor.visit( output->root.functionIndex( "*init*" ) ,
		[&]( uint32_t id , const bool exit ) -> bool {
			if ( exit || callInfo[ id ] & E_InstrRestriction::INIT ) {
				return false;
			}
			callInfo[ id ] |= E_InstrRestriction::INIT;
			return true;
		} );
	callGraphVisitor.visit( output->root.functionIndex( "*frame*" ) ,
		[&]( uint32_t id , const bool exit ) -> bool {
			if ( exit || callInfo[ id ] & E_InstrRestriction::FRAME ) {
				return false;
			}
			callInfo[ id ] |= E_InstrRestriction::FRAME;
			return true;
		} );
	for ( auto i = 0u ; i < output->root.nFunctions( ) ; ) {
		if ( !callInfo[ i ] ) {
			output->root.removeFunction( output->root.function( i ).name( ) );
			callInfo.removeSwap( i );
			continue;
		}
		visitor.visit( output->root.function( i ) , [&]( A_Node& node , bool exit ) {
			if ( exit ) {
				return false;
			}
			auto const* instr( dynamic_cast< A_InstructionNode const* >( &node ) );
			if ( instr && ( instr->restriction( ) & callInfo[ i ] ) ) {
				T_StringBuilder sb;
				sb << "instruction not allowed in "
					<< ( ( instr->restriction( ) & E_InstrRestriction::INIT )
						? "initialisation" : "frame function" );
				errors.addNew( std::move( sb ) , instr->location( ) );
			}
			return true;
		} );
		i ++;
	}
	return errors.empty( );
}

/* Find local variable declarations, add them to the function to owns them,
 * add errors if duplicates or variables with names that match function
 * arguments are found.
 */
bool T_ParserImpl_::checkLocalVariables( ) noexcept
{
	M_LOGSTR_( "... Checking local variables" , 2 );
	uint32_t cfi;
	T_StringBuilder esb;
	for ( cfi = 0 ; cfi < output->root.nFunctions( ) ; cfi ++ ) {
		auto& function( output->root.function( cfi ) );
		visitor.visit( function , [&]( A_Node& n , const bool exit ) -> bool {
			if ( exit || n.type( ) != A_Node::OP_LOCALS ) {
				return true;
			}

			auto& locals( dynamic_cast< T_LocalsInstrNode& >( n ) );
			for ( auto i = 0u ; i < locals.variables( ) ; i ++ ) {
				auto prev{ function.addLocalVariable(
						locals.varName( i ) ,
						locals.varLocation( i ) ) };
				if ( !prev ) {
					continue;
				}

				esb << "duplicate local '" << locals.varName( i )
					<< "'; previous declaration at "
					<< *prev;
				errors.addNew( std::move( esb ) , locals.varLocation( i ) );
			}

			return false;
		} );
	}
	return errors.empty( );
}

/* Find and determine the type of all global declarations. This includes
 * variables and resources. In addition, make sure no resources are
 * declared as local and that all declarations for a global name have
 * the same type.
 */
bool T_ParserImpl_::collectGlobalTypes( ) noexcept
{
	M_LOGSTR_( "... Collecting global declaration types" , 2 );

	// Temporary table for type / first declaration location
	struct T_Decl_ {
		E_DataType type;
		T_SRDLocation location;

		T_Decl_( const E_DataType dt ,
				T_SRDLocation const& loc ) noexcept
			: type( dt ) , location( loc )
		{ }

		T_Decl_( const E_DataType dt ) noexcept
			: type( dt ) , location{}
		{ }
	};
	T_KeyValueTable< T_String , T_Decl_ > type;
	type.add( T_String::Pooled( "time" ) , T_Decl_{ E_DataType::BUILTIN } );
	type.add( T_String::Pooled( "width" ) , T_Decl_{ E_DataType::BUILTIN } );
	type.add( T_String::Pooled( "height" ) , T_Decl_{ E_DataType::BUILTIN } );

	uint32_t cfi;
	T_StringBuilder esb;
	for ( cfi = 0 ; cfi < output->root.nFunctions( ) ; cfi ++ ) {
		auto& function( output->root.function( cfi ) );
		visitor.visit( function , [&]( A_Node& node , bool exit ) -> bool {
			if ( exit ) {
				return false;
			}

			// If the node defines or sets something, get its identifier,
			// location and type.
			E_DataType dt{ E_DataType::UNKNOWN };
			T_String id;
			T_SRDLocation location;
			if ( node.type( ) == A_Node::OP_SET ) {
				id = dynamic_cast< T_SetInstrNode& >( node ).id( );
				dt = E_DataType::VARIABLE;
				location = dynamic_cast< T_SetInstrNode& >( node ).idLocation( );
			} else {
				auto* np{ dynamic_cast< A_ResourceDefInstrNode* >( &node ) };
				if ( !np ) {
					return !dynamic_cast< A_ExpressionNode* >( &node );
				}
				dt = np->dataType( );
				id = np->id( );
				location = np->idLocation( );
			}
			assert( dt != E_DataType::UNKNOWN );

			logger( [&]{
				T_StringBuilder sb;
				sb << "id " << id << " as " << dt << " at " << location;
				return sb;
			} , 3 );

			// When we find a set instruction, we need to check whether
			// it is affecting a local variable. If it is we'll just skip
			// it.
			if ( function.hasLocal( id ) ) {
				if ( function.isArgument( id ) ) {
					errors.addNew( "trying to override argument",
							node.location( ) );
				} else if ( dt != E_DataType::VARIABLE ) {
					esb << "cannot define a local " << dt;
					errors.addNew( std::move( esb ) , node.location( ) );
				}
				return false;
			}

			// Add new entries
			T_Decl_ const* const existing( type.get( id ) );
			if ( !existing ) {
				type.add( id , T_Decl_{ dt , location } );
				return false;
			}

			// Make sure it matches previous declarations
			if ( existing->type == dt ) {
				return false;
			}
			if ( existing->type == E_DataType::BUILTIN ) {
				esb << "trying to redefine built-in variable " << id;
			} else {
				esb << "'" << id << "' redeclared as " << dt
					<< "; previous declaration as "
					<< existing->type << " at "
					<< existing->location;
			}
			errors.addNew( std::move( esb ) , location );

			return false;
		} );
	}

	// Copy type table to the output
	for ( auto const& k : type.keys( ) ) {
		output->types.add( k , type.get( k )->type );
	}

	return errors.empty( );
}

bool T_ParserImpl_::checkArgumentTypes( ) noexcept
{
	M_LOGSTR_( "... Checking function argument types" , 2 );

	// Find functions for which arguments types need to be resolved
	const auto nFunctions( output->root.nFunctions( ) );
	bool argsResolved[ nFunctions ];
	uint32_t nSolved = 0;
	for ( auto i = 0u ; i < nFunctions ; i ++ ) {
		auto& fn( output->root.function( i ) );
		if ( fn.type( ) == A_Node::DECL_FN ) {
			auto& rfn( dynamic_cast< T_FuncNode& >( fn ) );
			argsResolved[ i ] = rfn.arguments( ) == 0;
		} else {
			argsResolved[ i ] = true;
		}
		if ( argsResolved[ i ] ) {
			nSolved ++;
		}
	}
	// No functions use arguments -> we're done.
	if ( nSolved == nFunctions ) {
		return true;
	}

	// Find all calls with arguments
	T_MultiArray< T_CallInstrNode* > callInstuctions;
	for ( auto i = 0u ; i < nFunctions ; i ++ ) {
		callInstuctions.next( );
		visitor.visit( output->root.function( i ) , [&]( A_Node& node , const bool exit ) {
			if ( exit || node.type( ) != A_Node::OP_CALL ) {
				return true;
			}

			auto& call( dynamic_cast< T_CallInstrNode& >( node ) );
			if ( call.size( ) != 0 ) {
				callInstuctions.add( &call );
			}
			return false;
		} );
	}

#define M_TRACE_( l , x ) \
	logger( [&]{ \
		T_StringBuilder sb; \
		sb << x; \
		return sb; \
	} , l )

	T_StringBuilder esb;
	bool changed = true;
	while ( changed ) {
		changed = false;

		// Go through all functions for which argument types have been
		// resolved and check all calls.
		for ( auto i = 0u ; i < nFunctions ; i ++ ) {
			auto& f( output->root.function( i ) );
			M_TRACE_( 3 , "about to check function " << f.name( ) );
			if ( !argsResolved[ i ] ) {
				M_TRACE_( 3 , " -> arguments not resolved, skipped" );
				continue;
			}

			M_TRACE_( 3 , " -> " << callInstuctions.sizeOf( i ) << " calls w/ arguments" );
			for ( auto c = 0u ; c < callInstuctions.sizeOf( i ) ; c ++ ) {
				auto const* call( callInstuctions.get( i , c ) );
				if ( ! call ) {
					continue;
				}

				auto const& called( call->id( ) );
				const auto calledIdx( output->root.functionIndex( called ) );
				auto& calledFn( dynamic_cast< T_FuncNode& >(
							output->root.function( calledIdx ) ) );
				M_TRACE_( 4 , " -> checking call to " << called << " (idx " << calledIdx
						<< ") at " << call->location( ) );
				assert( call->size( ) == calledFn.arguments( ) );
				if ( argsResolved[ calledIdx ] ) {
					M_TRACE_( 4 , "    argument types already resolved, checking" );
				} else {
					M_TRACE_( 4 , "    resolving arguments" );
				}

				bool ok = true;
				for ( auto a = 0u ; a < call->size( ) ; a ++ ) {
					auto& arg( call->argument( a ).expression( ) );
					E_DataType ndt{ E_DataType::UNKNOWN };
					if ( arg.type( ) == A_Node::EXPR_ID ) {
						auto const& idn( dynamic_cast< T_IdentifierExprNode& >( arg ) );
						T_String const& id( idn.id( ) );
						ndt = getTypeOf( id , &f );
						if ( ndt == E_DataType::UNKNOWN ) {
							errors.addNew( "unknown identifier" ,
									arg.location( ) );
						}
					} else {
						ndt = E_DataType::VARIABLE;
					}
					M_TRACE_( 4 , "     [" << a << "] " << ndt );

					// Arguments not resolved -> try setting the argument's type
					if ( !argsResolved[ calledIdx ] ) {
						ok = ( ndt != E_DataType::UNKNOWN );
						calledFn.setLocalType( a , ndt );
						continue;
					}

					// Arguments resolved -> error if types don't match
					if ( ndt != E_DataType::UNKNOWN && calledFn.getLocalType( a ) != ndt ) {
						esb << "argument " << ( a + 1 ) << " of function '"
							<< called << "' should be a "
							<< calledFn.getLocalType( a )
							<< " but a " << ndt << " is being passed";
						errors.addNew( std::move( esb ) , arg.location( ) );
					}
				}
				argsResolved[ calledIdx ] = ok;
				changed = changed || ok;
				callInstuctions.get( i , c ) = nullptr;
			}
		}
	}

	return errors.empty( );
}

/*------------------------------------------------------------------------------*/

bool T_ParserImpl_::checkIdentifiers( ) noexcept
{
	M_LOGSTR_( "... Checking command argument types" , 2 );

	uint32_t cfi;
	T_StringBuilder esb;
	for ( cfi = 0 ; cfi < output->root.nFunctions( ) ; cfi ++ ) {
		auto& function( output->root.function( cfi ) );
		visitor.visit( function , [&]( A_Node& n , const bool exit ) -> bool {
			if ( exit ) {
				return true;
			}
			switch ( n.type( ) ) {

			    case A_Node::EXPR_INPUT:
				ciInput( cfi , n );
				return false;

			    case A_Node::EXPR_ID:
				ciIdentifier( cfi , n );
				return false;

			    case A_Node::OP_USE_FRAMEBUFFER:
			    {
				auto& instr( dynamic_cast< T_UseInstrNode& >( n ) );
				checkIdentifier( instr.id( ) , instr.idLocation( ) , cfi ,
						E_DataType::FRAMEBUFFER );
				return false;
			    }

			    case A_Node::OP_USE_PROGRAM:
			    {
				auto& instr( dynamic_cast< T_UseInstrNode& >( n ) );
				checkIdentifier( instr.id( ) , instr.idLocation( ) , cfi ,
						E_DataType::PROGRAM );
				return false;
			    }

			    case A_Node::OP_USE_PIPELINE:
			    {
				auto& instr( dynamic_cast< T_UseInstrNode& >( n ) );
				checkIdentifier( instr.id( ) , instr.idLocation( ) , cfi ,
						E_DataType::PIPELINE );
				return false;
			    }

			    case A_Node::OP_USE_TEXTURE:
			    {
				auto& instr( dynamic_cast< T_UseTextureInstrNode& >( n ) );
				checkIdentifier( instr.id( ) , instr.idLocation( ) , cfi ,
						E_DataType::TEXTURE );
				checkIdentifier( instr.samplerId( ) , instr.samplerIdLocation( ) ,
						cfi , E_DataType::SAMPLER );
				return true;
			    }

			    case A_Node::OP_IMAGE:
			    {
				auto& instr( dynamic_cast< T_ImageInstrNode& >( n ) );
				checkIdentifier( instr.id( ) , instr.idLocation( ) , cfi ,
						E_DataType::TEXTURE );
				return true;
			    }

			    case A_Node::OP_ODBG:
			    {
				auto& t( dynamic_cast< T_OutputDebugInstrNode& >( n ) );
				checkIdentifier( t.texture( ) , t.textureLocation( ) , cfi ,
						E_DataType::TEXTURE );
				return false;
			    }

			    case A_Node::OP_UNIFORMS:
			    {
				auto& t( dynamic_cast< T_UniformsInstrNode& >( n ) );
				checkIdentifier( t.progId( ) , t.progIdLocation( ) , cfi ,
						E_DataType::PROGRAM );
				return false;
			    }

			    case A_Node::OP_OVERRIDES:
			    {
				auto& ov{ dynamic_cast< T_OverridesInstrNode& >( n ) };
				ovVisitor.visitor.visit( &ov.root( ) ,
					[&]( T_SyncOverrideVisitor::T_Element element , bool exit ) -> bool {
						if ( exit || element.hasType< T_SyncOverrideSection* >( ) ) {
							return true;
						}

						auto& ovr( *element.value< A_SyncOverride* >( ) );
						auto const& in( ovr.inputNames( ) );
						const auto nin( in.size( ) );
						for ( auto i = 0u ; i < nin ; i ++ ) {
							checkIdentifier( in[ i ] , ovr.location( ) ,
									cfi , E_DataType::INPUT );
						}
						return false;
					}
				);
				return false;
			    }

			    default:
				return true;
			}
		} );
	}
	return errors.empty( );
}

void T_ParserImpl_::checkIdentifier(
		T_String const& id ,
		T_SRDLocation const& location ,
		const uint32_t funcIndex ,
		const E_DataType expected ) noexcept
{
	const E_DataType dt{ getTypeOf( id , &output->root.function( funcIndex ) ) };

	if ( dt == E_DataType::UNKNOWN ) {
		errors.addNew( "unknown identifier" , location );
	} else if ( dt != expected ) {
		T_StringBuilder esb;
		esb << expected << " expected, " << dt << " found instead";
		errors.addNew( std::move( esb ) , location );
	}
}

void T_ParserImpl_::ciInput(
		const uint32_t funcIndex ,
		A_Node& input ) noexcept
{
	auto& e( dynamic_cast< T_InputExprNode& >( input ) );
	auto const* const t( output->types.get( e.id( ) ) );
	if ( !t ) {
		errors.addNew( "no such input" , e.idLocation( ) );
	} else if ( *t != E_DataType::INPUT ) {
		T_StringBuilder esb;
		esb << "'" << e.id( ) << "' used as input but declared as a "
			<< *t;
		errors.addNew( std::move( esb ) , e.idLocation( ) );
	} else if ( callInfo[ funcIndex ] & E_InstrRestriction::INIT ) {
		errors.addNew( "input used in initialisation" ,
				e.location( ) );
	}
}

void T_ParserImpl_::ciIdentifier(
		const uint32_t funcIndex ,
		A_Node& input ) noexcept
{
	auto& e( dynamic_cast< T_IdentifierExprNode& >( input ) );
	E_DataType dt{ getTypeOf( e.id( ) , &output->root.function( funcIndex ) ) };

	// Undefined identifiers
	if ( dt == E_DataType::UNKNOWN ) {
		errors.addNew( "unknown identifier" , e.location( ) );
		return;
	}

	// Variables are fine
	if ( dt == E_DataType::VARIABLE ) {
		return;
	}

	// Check builtins
	if ( dt == E_DataType::BUILTIN ) {
		if ( ( callInfo[ funcIndex ] & E_InstrRestriction::INIT )
				&& e.id( ) == "time" ) {
			errors.addNew( "'time' built-in used in initialisation" ,
					e.location( ) );
		}
		return;
	}

	// Allow other types to be used as an expression
	// in the context of the call instruction.
	if ( e.parent( ).type( ) != A_Node::OP_CALL ) {
		T_StringBuilder esb;
		esb << "'" << e.id( )
			<< "' used as a variable but declared as a "
			<< dt;
		errors.addNew( std::move( esb ) , e.location( ) );
	}
}

/*------------------------------------------------------------------------------*/

E_DataType T_ParserImpl_::getTypeOf(
		T_String const& name ,
		A_FuncNode const* context ) const noexcept
{
	if ( context && context->hasLocal( name ) ) {
		return context->getLocalType( name );
	} else {
		auto const* const ptr( output->types.get( name ) );
		if ( ptr ) {
			return *ptr;
		} else {
			return E_DataType::UNKNOWN;
		}
	}
}

/*------------------------------------------------------------------------------*/

bool T_ParserImpl_::parseTopLevel(
		T_SRDList const& input ) noexcept
{
	M_LOGSTR_( "... Generating tree" , 2 );
	parseTLList( input );
	return errors.empty( );
}

void T_ParserImpl_::parseTLList(
		T_SRDList const& input ) noexcept
{
	for ( auto const& t : input ) {
		if ( t.type( ) == E_SRDTokenType::LIST && t.list( ).size( ) > 0 ) {
			parseTLListItem( t.list( ) );
		} else {
			errors.addNew( "top-level list expected" , t.location( ) );
		}
	}
}

void T_ParserImpl_::parseTLListItem(
		T_SRDList const& input ) noexcept
{
	assert( input.size( ) != 0 );
	auto const& ft{ input[ 0 ] };
	if ( ft.type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "word expected" , ft.location( ) );
		return;
	}

	auto const& fw{ ft.stringValue( ) };
	if ( fw == "include" ) {
		if ( input.size( ) == 1 ) {
			errors.addNew( "file name expected" ,
					ft.location( ) );
		} else {
			handleInclude( input[ 1 ] );
			if ( input.size( ) > 2 ) {
				errors.addNew( "too many arguments" ,
						input[ 2 ].location( ) );
			}
		}
	} else if ( fw == "init" || fw == "frame" || fw == "fn" ) {
		parseFunction( input );
	} else {
		errors.addNew( "function or include expected" , ft.location( ) );
	}
}

/*----------------------------------------------------------------------------*/

void T_ParserImpl_::handleInclude(
		T_SRDToken const& tFileName ) noexcept
{
	if ( tFileName.type( ) != E_SRDTokenType::STRING ) {
		errors.addNew( "file name expected" ,
				tFileName.location( ) );
		return;
	}

	// Resolve included file path
	T_FSPath inclPath{ tFileName.stringValue( ) };
	if ( !inclPath.isValid( ) ) {
		errors.addNew( "invalid file name" ,
				tFileName.location( ) );
		return;
	}
	if ( !inclPath.isAbsolute( ) ) {
		inclPath = curFile.parent( ) + inclPath;
	}
	inclPath = inclPath.canonical( );
	if ( !inclPath.isUnder( baseDir ) ) {
		errors.addNew( "file is not in the project's directory" ,
				tFileName.location( ) );
		return;
	}

	// Check for recursive includes
	if ( inclStack.contains( inclPath ) ) {
		errors.addNew( "recursive file inclusion" ,
				tFileName.location( ) );
		return;
	}

	// Load file
	const auto relPath{ inclPath.makeRelative( baseDir ) };
	T_SRDMemoryTarget srdOut;
	{
		T_File input{ inclPath , E_FileMode::READ_ONLY };
		try {
			input.open( );

		} catch ( X_StreamError const& e ) {
			T_StringBuilder sb;
			sb << "could not open '" << relPath << "': " << e.what( );
			if ( e.code( ) == E_StreamError::SYSTEM_ERROR ) {
				sb << " (error code " << e.systemError( ) << ")";
			}
			errors.addNew( std::move( sb ) , tFileName.location( ) );
			return;
		}

		srdOut.clearComments( true ).clearFlushToken( true );
		try {
			T_SRDTextReader srdReader{ srdOut };
			T_FileInputStream fis{ input };
			srdReader.read( relPath.toString( ) , fis );

		} catch ( X_StreamError const& e ) {
			T_StringBuilder sb;
			sb << "could not read '" << relPath << "': " << e.what( );
			if ( e.code( ) == E_StreamError::SYSTEM_ERROR ) {
				sb << " (error code " << e.systemError( ) << ")";
			}
			errors.addNew( std::move( sb ) , tFileName.location( ) );
			return;

		} catch ( X_SRDErrors const& e ) {
			const auto ne{ e.errors.size( ) };
			for ( auto i = 0u ; i < ne ; i ++ ) {
				errors.add( e.errors[ i ] );
			}
			return;
		}
	}

	// Parse included file
	inclStack.add( curFile );
	curFile = inclPath;
	output->files.add( inclPath );
	parseTLList( srdOut.list( ) );
	curFile = inclStack.last( );
	inclStack.removeLast( );
}

/*----------------------------------------------------------------------------*/

void T_ParserImpl_::parseFunction(
		T_SRDList const& funcList ) noexcept
{
	auto const& fw( funcList[ 0 ] );
	T_String const& ftw( fw.stringValue( ) );
	T_OwnPtr< A_FuncNode > fn;
	if ( ftw == "fn" ) {
		if ( funcList.size( ) < 3 ) {
			errors.addNew( "function name and arguments expected" ,
					fw.location( ) );
			return;
		}
		if ( funcList[ 1 ].type( ) != E_SRDTokenType::WORD ) {
			errors.addNew( "function name expected" , funcList[ 1 ].location( ) );
			return;
		}

		fn = NewOwned< T_FuncNode >( funcList[ 1 ].stringValue( ) , output->root );
		parseFunctionArguments( dynamic_cast< T_FuncNode& >( *fn ) ,
				funcList[ 2 ] );
	} else {
		fn = NewOwned< T_SpecialFuncNode >( ftw == "init" , output->root );
	}
	fn->location( ) = fw.location( );

	const auto af( output->root.addFunction( fn ) );
	if ( af.dupLocation ) {
		T_StringBuilder esb( "duplicate " );
		switch ( fn->type( ) ) {
			case A_Node::DECL_FN:
				esb << "function '" << fn->name( ) << "'";
				break;
			case A_Node::DECL_INIT:
				esb << "initialisation function";
				break;
			case A_Node::DECL_FRAME:
				esb << "frame function";
				break;
			default: std::abort( );
		}
		esb << "; previous declaration: " << *af.dupLocation;
		errors.addNew( std::move( esb ) , fw.location( ) );
	}

	parseInstructions( af.function.instructions( ) , funcList , ftw == "fn" ? 3 : 1 );
}

void T_ParserImpl_::parseFunctionArguments(
		T_FuncNode& function ,
		T_SRDToken const& argsToken ) noexcept
{
	if ( argsToken.type( ) != E_SRDTokenType::LIST ) {
		errors.addNew( "arguments list expected" , argsToken.location( ) );
		return;
	}
	for ( auto const& token : argsToken.list( ) ) {
		if ( token.type( ) != E_SRDTokenType::WORD ) {
			errors.addNew( "argument name expected" , token.location( ) );
			continue;
		}

		T_String const& id( token.stringValue( ) );
		if ( id == "time" || id == "width" || id == "height" ) {
			errors.addNew( "cannot use built-in as argument" ,
					token.location( ) );
			continue;
		}

		const auto rv( function.addArgument( token ) );
		if ( rv ) {
			T_StringBuilder esb;
			esb << "duplicate argument '" << token.stringValue( )
				<< "'; previous declaration: " << *rv;
			errors.addNew( std::move( esb ) , token.location( ) );
		}
	}
}

/*----------------------------------------------------------------------------*/

void T_ParserImpl_::parseInstructions(
		T_InstrListNode& instructions ,
		T_SRDList const& input ,
		const uint32_t start ) noexcept
{
	for ( auto iter( input.begin( ) + start ) ; iter.valid( ) ; iter ++ ) {
		T_SRDToken const& itok( *iter );
		if ( itok.type( ) != E_SRDTokenType::LIST ) {
			errors.addNew( "instruction expected" , itok.location( ) );
			continue;
		}

		T_SRDList const& ilist( itok.list( ) );
		if ( ilist.empty( ) ) {
			errors.addNew( "instruction expected" , itok.location( ) );
			continue;
		}

		T_SRDToken const& iname( ilist[ 0 ] );
		if ( iname.type( ) != E_SRDTokenType::WORD ) {
			errors.addNew( "instruction name expected" , iname.location( ) );
			continue;
		}

		T_String const& iword( iname.stringValue( ) );
		if ( !instrMap.contains( iword ) ) {
			errors.addNew( "unknown instruction" , iname.location( ) );
			continue;
		}

#define M_CASE_( NAME , FNAME ) case E_InstrType::NAME: parse##FNAME##Instruction( instructions , ilist ); break
		switch ( *instrMap.get( iword ) ) {
			M_CASE_( CALL , Call );
			M_CASE_( CLEAR , Clear );
			M_CASE_( COMPUTE , Compute );
			M_CASE_( FRAMEBUFFER , Framebuffer );
			M_CASE_( IF , If );
			M_CASE_( INPUT , Input );
			M_CASE_( LOCALS , Locals );
			M_CASE_( ODBG , ODebug );
			M_CASE_( OVERRIDES , Overrides );
			M_CASE_( PIPELINE , Pipeline );
			M_CASE_( PROFILE , Profile );
			M_CASE_( PROGRAM , Program );
			M_CASE_( SAMPLER , Sampler );
			M_CASE_( SET , Set );
			M_CASE_( TEXTURE , Texture );
			M_CASE_( UNIFORMS_FLT , Uniforms );
			M_CASE_( UNIFORMS_INT , UniformsI );
			M_CASE_( USE_FRAMEBUFFER , UseFramebuffer );
			M_CASE_( USE_PIPELINE , UsePipeline );
			M_CASE_( USE_PROGRAM , UseProgram );
			M_CASE_( USE_TEXTURE , UseTexture );
			M_CASE_( IMAGE , Image );
			M_CASE_( VIEWPORT , Viewport );

		    case E_InstrType::MAINOUT:
			if ( ilist.size( ) != 1 ) {
				errors.addNew( "too many arguments" , iname.location( ) );
			}
			instructions.add< T_MainOutputInstrNode >( ).location( ) = iname.location( );
			break;

		    case E_InstrType::FULLSCREEN:
			if ( ilist.size( ) != 1 ) {
				errors.addNew( "too many arguments" , iname.location( ) );
			}
			instructions.add< T_FullscreenInstrNode >( ).location( ) = iname.location( );
			break;
		}
#undef M_CASE_
	}
}

P_InstrListNode T_ParserImpl_::parseBlock(
		A_Node& parent ,
		T_SRDToken const& block ) noexcept
{
	if ( block.type( ) != E_SRDTokenType::LIST ) {
		errors.addNew( "block expected" , block.location( ) );
		return {};
	}

	P_InstrListNode rv{ NewOwned< T_InstrListNode >( parent ) };
	rv->location( ) = block.location( );
	parseInstructions( *rv , block.list( ) , 0 );
	return rv;
}

/*----------------------------------------------------------------------------*/

#define M_INSTR_( NAME ) \
	void T_ParserImpl_::parse##NAME##Instruction( \
			T_InstrListNode& instructions , \
			T_SRDList const& input ) noexcept

M_INSTR_( Call )
{
	if ( input.size( ) == 1 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "function identifier expected" ,
				input[ input.size( ) == 1 ? 0 : 1 ].location( ) );
		return;
	}

	auto& instr{ instructions.add< T_CallInstrNode >( input[ 1 ] ) };
	instr.location( ) = input[ 0 ].location( );
	for ( auto it = input.begin( ) + 2 ; it.valid( ) ; ++it ) {
		instr.addArgument( parseExpression( instr , *it ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Clear )
{
	if ( input.size( ) < 5 ) {
		errors.addNew( "not enough arguments" , input[ 0 ].location( ) );
	} else if ( input.size( ) > 5 ) {
		errors.addNew( "too many arguments" , input[ 0 ].location( ) );
	}

	auto& instr{ instructions.add< T_ClearInstrNode >( ) };
	instr.location( ) = input[ 0 ].location( );
	for ( auto i = 1u ; i < std::max( 5u , input.size( ) ) ; i ++ ) {
		instr.addComponent( parseExpression( instr , input[ i ] ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Compute )
{
	if ( input.size( ) < 4 ) {
		errors.addNew( "not enough arguments" , input[ 0 ].location( ) );
	} else if ( input.size( ) > 4 ) {
		errors.addNew( "too many arguments" , input[ 0 ].location( ) );
	}

	auto& instr{ instructions.add< T_ComputeInstrNode >( ) };
	instr.location( ) = input[ 0 ].location( );
	for ( auto i = 1u ; i < std::max( 4u , input.size( ) ) ; i ++ ) {
		instr.addComponent( parseExpression( instr , input[ i ] ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Framebuffer )
{
	if ( input.size( ) == 1 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "framebuffer identifier expected" ,
				input[ input.size( ) == 1 ? 0 : 1 ].location( ) );
		return;
	}

	auto& instr( instructions.add< T_FramebufferInstrNode >( input[ 1 ] ) );
	instr.location( ) = input[ 0 ].location( );

	bool ok( true );
	for ( auto i = 2u ; i < input.size( ) ; i ++ ) {
		ok = parseFramebufferEntry( instr , input[ i ] ) && ok;
	}
	if ( ok && instr.colorAttachments( ) == 0 && !instr.depthAttachment( ) ) {
		errors.addNew( "framebuffer has no attachments" ,
				input[ 0 ].location( ) );
	}
}

bool T_ParserImpl_::parseFramebufferEntry(
		T_FramebufferInstrNode& instruction ,
		T_SRDToken const& entry ) noexcept
{
	if ( entry.type( ) == E_SRDTokenType::WORD ) {
		const bool ok( instruction.addColorAttachment( entry ) );
		if ( !ok ) {
			errors.addNew( "duplicate color attachment" ,
					entry.location( ) );
		}
		return ok;
	}

	if ( entry.type( ) != E_SRDTokenType::LIST ) {
		errors.addNew( "framebuffer attachment expected" ,
				entry.location( ) );
		return false;
	}

	T_SRDList const& l( entry.list( ) );
	if ( l.size( ) < 2 || l.size( ) > 3
			|| l[ 0 ].type( ) != E_SRDTokenType::WORD
			|| l[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "invalid framebuffer attachment" ,
				entry.location( ) );
		return false;
	}

	P_ExpressionNode lod{ l.size( ) > 2
			? parseExpression( instruction , l[ 2 ] )
			: P_ExpressionNode{} };
	T_String const& atype( l[ 0 ].stringValue( ) );
	if ( atype == "depth" ) {
		const bool ok( instruction.setDepthAttachment(
					l[ 1 ] , std::move( lod ) ) );
		if ( !ok ) {
			errors.addNew( "duplicate depth attachment" ,
					entry.location( ) );
		}
		return ok;
	}

	if ( atype == "color" ) {
		const bool ok( instruction.addColorAttachment(
					l[ 1 ] , std::move( lod ) ) );
		if ( !ok ) {
			errors.addNew( "duplicate color attachment" ,
					l[ 1 ].location( ) );
		}
		return ok;
	}

	errors.addNew( "'color' or 'depth' expected" ,
			l[ 0 ].location( ) );
	return false;
}

/*----------------------------------------------------------------------------*/

M_INSTR_( If )
{
	if ( input.size( ) == 1 ) {
		errors.addNew( "expression and 'then' block expected" ,
				input[ 0 ].location( ) );
		return;
	}

	T_CondInstrNode& cond{ instructions.add< T_CondInstrNode >( ) };
	cond.location( ) = input[ 0 ].location( );
	cond.setExpression( parseExpression( cond , input[ 1 ] ) );
	if ( cond.hasExpression( ) ) {
		cond.expression( ).location( ) = input[ 1 ].location( );
	}

	if ( input.size( ) == 2 ) {
		errors.addNew( "'then' block expected" ,
				input[ 0 ].location( ) );
		return;
	}

	cond.setDefaultCase( parseBlock( cond , input[ 2 ] ) );
	if ( cond.hasDefaultCase( ) ) {
		cond.defaultCase( ).location( ) = input[ 2 ].location( );
	}

	if ( input.size( ) > 3 ) {
		cond.setCase( 0 , parseBlock( cond , input[ 3 ] ) );
		if ( cond.hasCase( 0 ) ) {
			cond.getCase( 0 ).location( ) = input[ 3 ].location( );
		}
		if ( input.size( ) > 4 ) {
			errors.addNew( "too many arguments" , input[ 4 ].location( ) );
		}
	} else {
		cond.setCase( 0 , NewOwned< T_InstrListNode >( cond ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Input )
{
	if ( input.size( ) < 2 || !input[ 1 ].isText( ) ) {
		errors.addNew( "input identifier expected" ,
				input[ input.size( ) < 2 ? 0 : 1 ].location( ) );
		return;
	}
	if ( input.size( ) > 3 ) {
		errors.addNew( "too many arguments" , input[ 3 ].location( ) );
	}
	if ( input.size( ) >= 3 && !input[ 2 ].isNumeric( ) ) {
		errors.addNew( "default value expected" , input[ 2 ].location( ) );
	}

	const bool hasDefault( input.size( ) >= 3 && input[ 2 ].isNumeric( ) );
	auto& instr( ([&]() -> T_InputInstrNode& {
		if ( hasDefault ) {
			return instructions.add< T_InputInstrNode >(
					input[ 1 ] , input[ 2 ] );
		}
		return instructions.add< T_InputInstrNode >( input[ 1 ] );
	})( ) );
	instr.location( ) = input[ 0 ].location( );
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Locals )
{
	const auto ni( input.size( ) );
	if ( ni < 2 ) {
		errors.addNew( "variable identifier expected" ,
				input[ 0 ].location( ) );
		return;
	}

	auto& instr{ instructions.add< T_LocalsInstrNode >( ) };
	instr.location( ) = input[ 0 ].location( );

	T_StringBuilder sb;
	for ( auto i = 1u ; i < ni ; i ++ ) {
		auto const& token( input[ i ] );
		if ( token.type( ) != E_SRDTokenType::WORD ) {
			errors.addNew( "variable identifier expected" ,
					token.location( ) );
			continue;
		}

		T_String const& id( token.stringValue( ) );
		if ( id == "time" || id == "width" || id == "height" ) {
			errors.addNew( "built-in cannot be declared local" ,
					token.location( ) );
			continue;
		}

		const auto prev{ instr.addVariable( token ) };
		if ( !prev ) {
			continue;
		}
		sb << "duplicate local '" << token.stringValue( )
			<< "'; previous declaration at "
			<< *prev;
		errors.addNew( std::move( sb ) , token.location( ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( ODebug )
{
	const auto ni( input.size( ) );
	if ( ni == 1 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "texture identifier expected" ,
				input[ ni == 1 ? 0 : 1 ].location( ) );
		return;
	}

	if ( ni == 2 || input[ 2 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "display mode expected" ,
				input[ ni == 2 ? 0 : 2 ].location( ) );
		return;
	}
	auto const* const mode( odbgModes.get( input[ 2 ].stringValue( ) ) );
	if ( !mode ) {
		errors.addNew( "invalid display mode" , input[ 2 ].location( ) );
		return;
	}

	if ( ni == 3 || !input[ 3 ].isText( ) ) {
		errors.addNew( "description expected" ,
				input[ ni == 3 ? 0 : 3 ].location( ) );
		return;
	}

	auto& instr{ instructions.add< T_OutputDebugInstrNode >(
			input[ 1 ] , *mode , input[ 2 ].location( ) ,
			input[ 3 ] ) };
	instr.location( ) = input[ 0 ].location( );

	if ( ni > 4 ) {
		errors.addNew( "too many arguments" , input[ 4 ].location( ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Overrides )
{
	if ( input.size( ) == 1 ) {
		errors.addNew( "not enough arguments" , input[ 0 ].location( ) );
		return;
	}

	struct T_StackEntry_ {
		T_SRDList const* list;
		uint32_t pos;

		T_StackEntry_( T_SRDList const* list ,
				const uint32_t pos ) noexcept
			: list( list ) , pos( pos )
		{}
	};
	T_AutoArray< T_StackEntry_ , 16 > stack;
	T_StackEntry_ current{ &input , 1  };
	T_SRDErrors ovParserErrors;
	bool hadException{ false };
	try {
		ovParser.start( ovParserErrors );
		while ( current.pos < current.list->size( ) || !stack.empty( ) ) {
			if ( current.pos == current.list->size( ) ) {
				current = stack.last( );
				stack.removeLast( );

				T_SRDToken ls{ T_SRDToken::ListEnd( ) };
				ls.location( (*current.list)[ current.pos ].location( ) );
				ovParser.push( ovParserErrors , std::move( ls ) );

				current.pos ++;
				continue;
			}

			auto const& src( (*current.list)[ current.pos ] );
			if ( src.type( ) == E_SRDTokenType::LIST ) {
				T_SRDToken ls{ T_SRDToken::ListStart( ) };
				ls.location( src.location( ) );
				ovParser.push( ovParserErrors , std::move( ls ) );
				stack.add( current );
				current = T_StackEntry_{ &src.list( ) , 0 };
			} else {
				ovParser.push( ovParserErrors , T_SRDToken{ src } );
				current.pos ++;
			}
		}
		ovParser.end( ovParserErrors );
	} catch ( X_SRDErrors const& e ) {
		hadException = true;
	}

	const auto n{ ovParserErrors.size( ) - ( hadException ? 1 : 0 ) };
	if ( n != 0 ) {
		for ( auto i = 0u ; i < n ; i ++ ) {
			errors.add( ovParserErrors[ i ] );
		}
		if ( hadException ) {
			errors.addNew( "too many errors in UI overrides list" ,
					input[ 0 ].location( ) );
		}
		return;
	}

	auto& instr{ instructions.add< T_OverridesInstrNode >(
			ovParser.getData< T_SharedPtr< T_SyncOverrideSection > >( ).makeOwned( ) ) };
	instr.location( ) = input[ 0 ].location( );
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Pipeline )
{
	if ( input.size( ) < 3 ) {
		errors.addNew( "identifier and program identifiers expected" ,
				input[ 0 ].location( ) );
		return;
	}

	const bool validId( input[ 1 ].type( ) == E_SRDTokenType::WORD );
	if ( !validId ) {
		errors.addNew( "pipeline identifier expected" , input[ 1 ].location( ) );
		return;
	}

	auto& pipeline{ instructions.add< T_PipelineInstrNode >( input[ 1 ] ) };
	pipeline.location( ) = input[ 0 ].location( );

	const auto nMax{ std::min( input.size( ) , 8u ) };
	for ( auto i = 2u ; i < nMax ; i ++ ) {
		T_SRDToken const& tok( input[ i ] );
		if ( tok.type( ) != E_SRDTokenType::WORD ) {
			errors.addNew( "program identifier expected" ,
					tok.location( ) );
			continue;
		}
		const auto dup( pipeline.addProgram( tok ) );
		if ( dup ) {
			T_StringBuilder esb;
			esb << "duplicate program identifier; previous use: " << *dup;
			errors.addNew( std::move( esb ) , tok.location( ) );
		}
	}

	if ( input.size( ) > 8 ) {
		errors.addNew( "too many arguments" , input[ 8 ].location( ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Profile )
{
	const bool hasEnough( input.size( ) < 2 );
	if ( hasEnough || !input[ 1 ].isText( ) ) {
		errors.addNew( "profiling section name expected" ,
				hasEnough ? input[ 1 ].location( ) : T_SRDLocation{} );
		if ( !hasEnough ) {
			return;
		}
	}

	const T_String text( input[ 1 ].isText( ) ? input[ 1 ].stringValue( ) : "*invalid*" );
	T_ProfileInstrNode& profile{ instructions.add< T_ProfileInstrNode >( text ) };
	profile.location( ) = input[ 0 ].location( );
	parseInstructions( profile.instructions( ) , input , 2 );
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Program )
{
	bool ok{ true };
	if ( input.size( ) == 1 ) {
		errors.addNew( "identifier and program name required" ,
				input[ 0 ].location( ) );
		return;
	}
	if ( input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "identifier (word) expected" , input[ 1 ].location( ) );
		ok = false;
	}
	if ( input.size( ) == 2 ) {
		errors.addNew( "program name required" , input[ 0 ].location( ) );
		return;
	}
	if ( !input[ 2 ].isText( ) ) {
		errors.addNew( "program name (string or word) expected" ,
				input[ 2 ].location( ) );
		ok = false;
	}

	if ( input.size( ) > 3 ) {
		errors.addNew( "too many arguments" , input[ 3 ].location( ) );
	}
	if ( !ok ) {
		return;
	}

	T_FSPath path{ input[ 2 ].stringValue( ) };
	if ( path.isAbsolute( ) ) {
		path = path.makeRelative( path.root( ) );
		path = ( Common::Project( ).basePath( ) + path ).canonical( );
	} else {
		path = ( curFile.parent( ) + path ).canonical( );
	}
	if ( !path.isUnder( Common::Project( ).basePath( ) ) ) {
		errors.addNew( "shader is not in project directory" ,
				input[ 2 ].location( ) );
	}

	T_ProgramInstrNode& program{ instructions.add< T_ProgramInstrNode >(
				input[ 1 ] , std::move( path ) ,
				input[ 2 ].location( ) ) };
	program.location( ) = input[ 0 ].location( );
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Sampler )
{
	const auto ni{ input.size( ) };
	if ( ni == 1 || input[ 0 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "sampler identifier expected" ,
				input[ ni == 1 ? 0 : 1 ].location( ) );
		return;
	}

	auto& instr{ instructions.add< T_SamplerInstrNode >( input[ 1 ] ) };
	instr.location( ) = input[ 0 ].location( );

	for ( auto i = 2u ; i < ni ; i ++ ) {
		T_SRDToken const& token( input[ i ] );
		if ( token.type( ) != E_SRDTokenType::LIST || token.list( ).empty( ) ) {
			errors.addNew( "list expected" , token.location( ) );
			continue;
		}

		T_SRDToken const& et( token.list( )[ 0 ] );
		if ( et.type( ) != E_SRDTokenType::WORD ) {
			errors.addNew( "'sampling', 'mipmaps', 'wrapping' or 'lod' expected" ,
					et.location( ) );
			continue;
		}

		T_String const& etn( et.stringValue( ) );
		if ( etn == "sampling" ) {
			parseSamplerSampling( instr , token.list( ) );
		} else if ( etn == "mipmaps" ) {
			parseSamplerMipmaps( instr , token.list( ) );
		} else if ( etn == "wrapping" ) {
			parseSamplerWrapping( instr , token.list( ) );
		} else if ( etn == "lod" ) {
			parseSamplerLOD( instr , token.list( ) );
		} else {
			errors.addNew( "'sampling', 'mipmaps', 'wrapping' or 'lod' expected" ,
					et.location( ) );
		}
	}
}

void T_ParserImpl_::parseSamplerSampling(
		T_SamplerInstrNode& instruction ,
		T_SRDList const& input )
{
	if ( input.size( ) == 1 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "sampling mode expected" ,
				input[ input.size( ) == 1 ? 0 : 1 ].location( ) );
		return;
	}

	bool ok = true;
	T_String const& sMode{ input[ 1 ].stringValue( ) };
	E_TexSampling smp{ E_TexSampling::NEAREST };
	if ( sMode == "linear" ) {
		smp = E_TexSampling::LINEAR;
	} else if ( sMode != "nearest" ) {
		errors.addNew( "'nearest' or 'linear' expected" , input[ 1 ].location( ) );
		ok = false;
	}

	if ( input.size( ) > 2 ) {
		errors.addNew( "too many arguments" , input[ 2 ].location( ) );
	}

	const auto prev{ instruction.setSampling( smp , input[ 0 ].location( ) ) };
	if ( prev && ok ) {
		T_StringBuilder sb;
		sb << "sampling mode already set at " << *prev;
		errors.addNew( std::move( sb ) , input[ 0 ].location( ) );
	}
}

void T_ParserImpl_::parseSamplerMipmaps(
		T_SamplerInstrNode& instruction ,
		T_SRDList const& input )
{
	if ( input.size( ) == 1 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "mipmap sampling mode expected" ,
				input[ input.size( ) == 1 ? 0 : 1 ].location( ) );
		return;
	}

	bool ok = true;
	T_String const& sMode{ input[ 1 ].stringValue( ) };
	T_Optional< E_TexSampling > smp{ };
	if ( sMode == "linear" ) {
		smp = E_TexSampling::LINEAR;
	} else if ( sMode == "nearest" ) {
		smp = E_TexSampling::NEAREST;
	} else if ( sMode != "no" ) {
		errors.addNew( "'no', 'nearest' or 'linear' expected" , input[ 1 ].location( ) );
		ok = false;
	}

	if ( input.size( ) > 2 ) {
		errors.addNew( "too many arguments" , input[ 2 ].location( ) );
	}

	const auto prev{ smp
		? instruction.setMipmapSampling( *smp , input[ 0 ].location( ) )
		: instruction.setNoMipmap( input[ 0 ].location( ) ) };
	if ( prev && ok ) {
		T_StringBuilder sb;
		sb << "mipmap sampling mode already set at " << *prev;
		errors.addNew( std::move( sb ) , input[ 0 ].location( ) );
	}
}

void T_ParserImpl_::parseSamplerWrapping(
		T_SamplerInstrNode& instruction ,
		T_SRDList const& input )
{
	if ( input.size( ) == 1 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "wrapping mode expected" ,
				input[ input.size( ) == 1 ? 0 : 1 ].location( ) );
		return;
	}

	bool ok = true;
	T_String const& sMode{ input[ 1 ].stringValue( ) };
	E_TexWrap smp{ E_TexWrap::REPEAT };
	if ( sMode == "clamp-border" ) {
		smp = E_TexWrap::CLAMP_BORDER;
	} else if ( sMode == "clamp-edge" ) {
		smp = E_TexWrap::CLAMP_EDGE;
	} else if ( sMode != "repeat" ) {
		errors.addNew( "'repeat', 'clamp-border' or 'clamp-edge' expected" ,
				input[ 1 ].location( ) );
		ok = false;
	}

	if ( input.size( ) > 2 ) {
		errors.addNew( "too many arguments" , input[ 2 ].location( ) );
	}

	const auto prev{ instruction.setWrapping( smp , input[ 0 ].location( ) ) };
	if ( prev && ok ) {
		T_StringBuilder sb;
		sb << "wrapping mode already set at " << *prev;
		errors.addNew( std::move( sb ) , input[ 0 ].location( ) );
	}
}

void T_ParserImpl_::parseSamplerLOD(
		T_SamplerInstrNode& instruction ,
		T_SRDList const& input )
{
	if ( input.size( ) < 3 ) {
		errors.addNew( "min/max LODs expected" , input[ 0 ].location( ) );
		return;
	}
	if ( input.size( ) > 3 ) {
		errors.addNew( "too many arguments" , input[ 2 ].location( ) );
	}

	P_ExpressionNode min{ parseExpression( instruction , input[ 1 ] ) };
	P_ExpressionNode max{ parseExpression( instruction , input[ 2 ] ) };
	const auto prev{ instruction.setLOD( input[ 0 ].location( ) ,
			std::move( min ) , std::move( max ) ) };
	if ( prev ) {
		T_StringBuilder sb;
		sb << "min/max LOD already set at " << *prev;
		errors.addNew( std::move( sb ) , input[ 0 ].location( ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Set )
{
	bool ok{ true };
	if ( input.size( ) == 1 ) {
		errors.addNew( "identifier and expression required" ,
				input[ 0 ].location( ) );
		return;
	}
	if ( input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "variable identifier expected" , input[ 1 ].location( ) );
		ok = false;
	}
	if ( input.size( ) == 2 ) {
		errors.addNew( "expression required" , input[ 0 ].location( ) );
	}
	if ( input.size( ) > 3 ) {
		errors.addNew( "too many arguments" , input[ 3 ].location( ) );
	}
	if ( !ok ) {
		return;
	}

	T_SetInstrNode& set{ instructions.add< T_SetInstrNode >( input[ 1 ] ) };
	set.location( ) = input[ 0 ].location( );
	if ( input.size( ) > 2 ) {
		auto expr( parseExpression( set , input[ 2 ] ) );
		if ( expr ) {
			set.setExpression( std::move( expr ) );
		}
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Texture )
{
	if ( input.size( ) < 2 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "texture identifier expected" ,
				( input.size( ) < 2 ? input[ 0 ] : input[ 1 ] ).location( ) );
		return;
	}
	if ( input.size( ) < 3 || input[ 2 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "texture type expected" ,
				( input.size( ) < 3 ? input[ 0 ] : input[ 2 ] ).location( ) );
		return;
	}

	auto const* const ttt( texTypeMap.get( input[ 2 ].stringValue( ) ) );
	if ( !ttt ) {
		errors.addNew( "invalid texture type" ,
				( input.size( ) < 3 ? input[ 0 ] : input[ 2 ] ).location( ) );
	}
	const auto tt( ttt ? *ttt : E_TexType::RGB8 );

	auto& instr{ instructions.add< T_TextureInstrNode >( input[ 1 ] , tt ) };
	instr.location( ) = input[ 0 ].location( );
	if ( input.size( ) > 4 ) {
		instr.setWidth( parseExpression( instr , input[ 3 ] ) );
	} else {
		errors.addNew( "width expected" , input[ 0 ].location( ) );
	}
	if ( input.size( ) > 4 ) {
		instr.setHeight( parseExpression( instr , input[ 4 ] ) );
	} else {
		errors.addNew( "height expected" , input[ 0 ].location( ) );
	}
	if ( input.size( ) > 5 ) {
		instr.setLODs( parseExpression( instr , input[ 5 ] ) );
	}
	if ( input.size( ) > 6 ) {
		errors.addNew( "too many arguments" , input[ 6 ].location( ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Uniforms )
{
	parseUniformsCommon( instructions , input , false );
}

M_INSTR_( UniformsI )
{
	parseUniformsCommon( instructions , input , true );
}

void T_ParserImpl_::parseUniformsCommon(
		T_InstrListNode& instructions ,
		T_SRDList const& input ,
		const bool integers ) noexcept
{
	if ( input.size( ) == 1 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "program identifier expected" ,
				input[ input.size( ) == 1 ? 0 : 1 ].location( ) );
		return;
	}
	if ( input.size( ) == 2 || !input[ 2 ].isInteger( ) ) {
		errors.addNew( "uniform location expected" ,
				input[ input.size( ) == 2 ? 0 : 2 ].location( ) );
		return;
	}
	if ( input[ 2 ].longValue( ) < 0 || input[ 2 ].longValue( ) > UINT32_MAX ) {
		errors.addNew( "invalid uniform location" , input[ 2 ].location( ) );
		return;
	}

	auto& instr{ instructions.add< T_UniformsInstrNode >(
			integers , input[ 1 ] , input[ 2 ] ) };
	instr.location( ) = input[ 0 ].location( );
	if ( input.size( ) == 3 ) {
		errors.addNew( "at least one value required" , input[ 0 ].location( ) );
		return;
	}

	for ( auto i = 3u ; i < input.size( ) ; i ++ ) {
		if ( i == 7 ) {
			errors.addNew( "too many arguments" , input[ i ].location( ) );
			return;
		}
		instr.addValue( parseExpression( instr , input[ i ] ) );
	}
}


/*----------------------------------------------------------------------------*/

M_INSTR_( UseFramebuffer )
{
	parseUseCommon( instructions , input , T_UseInstrNode::FRAMEBUFFER );
}

M_INSTR_( UsePipeline )
{
	parseUseCommon( instructions , input , T_UseInstrNode::PIPELINE );
}

M_INSTR_( UseProgram )
{
	parseUseCommon( instructions , input , T_UseInstrNode::PROGRAM );
}

void T_ParserImpl_::parseUseCommon(
		T_InstrListNode& instructions ,
		T_SRDList const& input ,
		T_UseInstrNode::E_Type type ) noexcept
{
	if ( input.size( ) == 1 || input[ 1 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "resource identifier expected" ,
				input[ input.size( ) == 1 ? 0 : 1 ].location( ) );
		return;
	}
	if ( input.size( ) > 2 ) {
		errors.addNew( "too many arguments" , input[ 2 ].location( ) );
	}

	auto& instr{ instructions.add< T_UseInstrNode >( type , input[ 1 ] ) };
	instr.location( ) = input[ 0 ].location( );
}

/*----------------------------------------------------------------------------*/

M_INSTR_( UseTexture )
{
	if ( input.size( ) == 1 || !input[ 1 ].isInteger( ) ) {
		errors.addNew( "bank number expected" ,
				input[ input.size( ) == 1 ? 0 : 1 ].location( ) );
		return;
	}
	if ( input[ 1 ].longValue( ) < 0 || input[ 1 ].longValue( ) > UINT32_MAX ) {
		errors.addNew( "invalid bank number" , input[ 1 ].location( ) );
		return;
	}

	if ( input.size( ) == 2 || input[ 2 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "texture identifier expected" ,
				input[ input.size( ) == 2 ? 0 : 2 ].location( ) );
		return;
	}
	if ( input.size( ) == 3 || input[ 3 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "sampler identifier expected" ,
				input[ input.size( ) == 3 ? 0 : 3 ].location( ) );
		return;
	}
	if ( input.size( ) > 4 ) {
		errors.addNew( "too many arguments" , input[ 4 ].location( ) );
	}

	auto& instr{ instructions.add< T_UseTextureInstrNode >(
			input[ 1 ] , input[ 2 ] , input[ 3 ] ) };
	instr.location( ) = input[ 0 ].location( );
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Image )
{
	// (image <binding expr> <tex id> <level expr> [{read|write|rw}]
	// 	[(layer <layer expr)]
	// )
	if ( input.size( ) == 1 ) {
		errors.addNew( "binding unit expression expected" ,
				input[ 0 ].location( ) );
		return;
	}
	if ( input.size( ) == 2 || input[ 2 ].type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "texture identifier expected" ,
				input[ input.size( ) == 2 ? 0 : 2 ].location( ) );
		return;
	}
	if ( input.size( ) == 3 ) {
		errors.addNew( "LOD level expression expected" ,
				input[ 2 ].location( ) );
		return;
	}

	auto& instr{ instructions.add< T_ImageInstrNode >( ) };
	instr.location( ) = input[ 0 ].location( );
	instr.id( input[ 2 ].stringValue( ) , input[ 2 ].location( ) );
	instr.setParameter( T_ImageInstrNode::P_UNIT ,
			parseExpression( instr , input[ 1 ] ) );
	instr.setParameter( T_ImageInstrNode::P_LEVEL ,
			parseExpression( instr , input[ 3 ] ) );

	using E_AM_ = T_ImageInstrNode::E_AccessMode;
	using T_AM_ = T_ImageInstrNode::T_AccessMode;
	T_AM_ mode{ E_AM_::READ , E_AM_::WRITE };

	uint32_t nck = 4;
	if ( nck < input.size( ) && input[ nck ].type( ) == E_SRDTokenType::WORD ) {
		auto const& v{ input[ nck ].stringValue( ) };
		if ( v == "read" ) {
			mode = E_AM_::READ;
		} else if ( v == "write" ) {
			mode = E_AM_::WRITE;
		} else if ( v != "rw" ) {
			errors.addNew( "invalid access mode" ,
					input[ nck ].location( ) );
		}
		nck ++;
	}
	instr.accessMode( mode );

	if ( nck < input.size( ) && input[ nck ].type( ) == E_SRDTokenType::LIST ) {
		auto const& lst{ input[ nck ].list( ) };
		if ( lst.size( ) < 2 || lst[ 0 ].type( ) != E_SRDTokenType::WORD
				|| lst[ 0 ].stringValue( ) != "layer" ) {
			errors.addNew( "invalid layer specification" ,
					lst.size( ) ? lst[ 0 ].location( )
						: input[ nck].location( ) );
		} else {
			instr.setParameter( T_ImageInstrNode::P_LAYER ,
					parseExpression( instr , lst[ 1 ] ) );
			if ( lst.size( ) > 2 ) {
				errors.addNew( "too many arguments" ,
						lst[ 2 ].location( ) );
			}
		}
		nck ++;
	}

	if ( nck < input.size( ) ) {
		errors.addNew( "too many arguments" , input[ nck ].location( ) );
	}
}

/*----------------------------------------------------------------------------*/

M_INSTR_( Viewport )
{
	auto& instr{ instructions.add< T_ViewportInstrNode >( ) };
	instr.location( ) = input[ 0 ].location( );

	for ( auto i = 1u ; i < 5 ; i ++ ) {
		T_ViewportInstrNode::E_Parameter p{
			T_ViewportInstrNode::E_Parameter( i - 1 ) };
		if ( input.size( ) < i ) {
			T_StringBuilder sb;
			sb << "missing ";
			switch ( p ) {
			    case T_ViewportInstrNode::PX: sb << "X"; break;
			    case T_ViewportInstrNode::PY: sb << "Y"; break;
			    case T_ViewportInstrNode::PWIDTH: sb << "width"; break;
			    case T_ViewportInstrNode::PHEIGHT: sb << "height"; break;
			}
			sb << " parameter";
			errors.addNew( std::move( sb ) , input[ 0 ].location( ) );
			return;
		} else {
			instr.setParameter( p , parseExpression( instr , input[ i ] ) );
		}
	}
}

/*----------------------------------------------------------------------------*/

P_ExpressionNode T_ParserImpl_::parseExpression(
		A_Node& parent ,
		T_SRDToken const& token ) noexcept
{
	if ( token.isNumeric( ) ) {
		return NewOwned< T_ConstantExprNode >( parent , token );
	}
	if ( token.type( ) == E_SRDTokenType::WORD || token.type( ) == E_SRDTokenType::VAR ) {
		return NewOwned< T_IdentifierExprNode >( parent , token );
	}

	if ( token.type( ) == E_SRDTokenType::LIST && !token.list( ).empty( ) ) {
		return parseOperation( parent , token.list( ) );
	}

	errors.addNew( "invalid expression" , token.location( ) );
	return {};
}

P_ExpressionNode T_ParserImpl_::parseOperation(
		A_Node& parent ,
		T_SRDList const& opList ) noexcept
{
	T_SRDToken const& opId( opList[ 0 ] );
	if ( opId.type( ) != E_SRDTokenType::WORD ) {
		errors.addNew( "operator expected" , opId.location( ) );
		return {};
	}

	if ( opId.stringValue( ) == "get-input" ) {
		if ( opList.size( ) == 1 || !opList[ 1 ].isText( ) ) {
			errors.addNew( "input identifier expected" ,
					opList[ opList.size( ) == 1 ? 0 : 1 ].location( ) );
			return { };
		}
		if ( opList.size( ) > 2 ) {
			errors.addNew( "too many arguments" , opList[ 2 ].location( ) );
		}
		auto node{ NewOwned< T_InputExprNode >( parent , opList[ 1 ] ) };
		node->location( ) = opList[ 0 ].location( );
		return node;
	}

	if ( binOpMap.contains( opId.stringValue( ) ) ) {
		return parseBinOp( parent , opList ,
				*binOpMap.get( opId.stringValue( ) ) );
	}
	if ( unaryOpMap.contains( opId.stringValue( ) ) ) {
		return parseUnaryOp( parent , opList ,
				*unaryOpMap.get( opId.stringValue( ) ) );
	}

	errors.addNew( "unknown operator" , opId.location( ) );
	return {};
}

P_ExpressionNode T_ParserImpl_::parseBinOp(
		A_Node& parent ,
		T_SRDList const& opList ,
		T_BinaryOperatorNode::E_Operator op ) noexcept
{
	if ( opList.size( ) < 3 ) {
		errors.addNew( "not enough arguments" , opList[ 0 ].location( ) );
	} else if ( opList.size( ) > 3 ) {
		errors.addNew( "too many arguments" , opList[ 3 ].location( ) );
	}

	T_OwnPtr< T_BinaryOperatorNode > opNode{
		NewOwned< T_BinaryOperatorNode >( parent , op ) };
	opNode->location( ) = opList[ 0 ].location( );

	if ( opList.size( ) > 1 ) {
		auto left{ parseExpression( *opNode , opList[ 1 ] ) };
		if ( left ) {
			opNode->setLeft( std::move( left ) );
		}
	}
	if ( opList.size( ) > 2 ) {
		auto right{ parseExpression( *opNode , opList[ 2 ] ) };
		if ( right ) {
			opNode->setRight( std::move( right ) );
		}
	}

	return opNode;
}

P_ExpressionNode T_ParserImpl_::parseUnaryOp(
		A_Node& parent ,
		T_SRDList const& opList ,
		T_UnaryOperatorNode::E_Operator op ) noexcept
{
	if ( opList.size( ) < 2 ) {
		errors.addNew( "not enough arguments" , opList[ 0 ].location( ) );
	} else if ( opList.size( ) > 2 ) {
		errors.addNew( "too many arguments" , opList[ 2 ].location( ) );
	}

	T_OwnPtr< T_UnaryOperatorNode > opNode{
		NewOwned< T_UnaryOperatorNode >( parent , op ) };
	opNode->location( ) = opList[ 0 ].location( );

	if ( opList.size( ) > 1 ) {
		auto argument{ parseExpression( *opNode , opList[ 1 ] ) };
		if ( argument ) {
			opNode->setArgument( std::move( argument ) );
		}
	}

	return opNode;
}

} // namespace

/*----------------------------------------------------------------------------*/

T_OpsParser::T_OpsParser( ) noexcept
	: A_PrivateImplementation( new T_ParserImpl_(
				&errors_ , &output_ , &logger_ ) ) ,
		errors_( 64 ) , output_{}
{}

bool T_OpsParser::parse(
		T_FSPath const& filePath ,
		T_SRDList const& input ) noexcept
{
	errors_.clear( );
	output_ = NewOwned< T_OpsParserOutput >( );
	p< T_ParserImpl_ >( ).main( filePath , input );
	return errors_.empty( );
}