/** * A sample TLM which reads an s-expression configuration file. * * The parser below is a recursive descent parser sitting on top of the * toolkit's DSNLEXER. You do not write a lexer and you do not compare strings: * the keywords of your grammar are listed one per line in * sexpr_config.keywords, and at build time CMake turns that list into two * generated files (see CMakeLists.txt): * * sexpr_config_lexer.h class SEXPR_CONFIG_LEXER, and namespace CFG_T * holding an enum value T_ for each line * sexpr_config_keywords.cc the keyword table the lexer looks names up in * * Both land in the build directory, never in your source directory. All that * is left for you to write is the grammar, which is the Parse() and parsePort() * functions below. */ #include #include #include // Generated from sexpr_config.keywords into the build directory. #include // The token enum lives in the namespace given as make_lexer()'s last argument, // so that T_port, T_baud and friends can be written unqualified here. using namespace CFG_T; // This TLM's own error codes. They travel with the message to the runtime log. #define E_NO_CONFIG_FILE 1 #define E_BAD_CONFIG_FILE 2 #define CONFIG_FILE "SEXPRCFG.LST" /// One (port ...) block of the configuration file. struct PORT { std::string name; int baud; std::string tag; PORT() : baud( 9600 ) {} }; static std::vector Ports; static int Version; DECLARE_TLM( TLM, "SEXPRCFG", 0 ) /** * Class CONFIG_PARSER * is the recursive descent parser for CONFIG_FILE. It derives from the * generated lexer, which puts NextTok(), CurText(), Expecting() and the rest * directly in scope. * * The pattern to copy is one function per nesting level of the grammar. Each * such function is entered just after its own opening '(' and keyword have been * read, and returns having consumed its matching ')'. Anything the grammar does * not allow is reported by Expecting() or Unexpected(), which throw a * PARSE_ERROR carrying the file, the offending line, and the line and column * numbers -- so you never write that error handling yourself. */ class CONFIG_PARSER : public SEXPR_CONFIG_LEXER { public: CONFIG_PARSER( FILE* aFile, const std::string& aFilename ) : SEXPR_CONFIG_LEXER( aFile, aFilename ) { } /** * Function Parse * reads the whole file: * (sexpr_config (version 1) (port ...) (port ...) ) */ void Parse() { T tok; NeedLEFT(); // '(' or PARSE_ERROR if( (tok = NextTok()) != T_sexpr_config ) Expecting( T_sexpr_config ); while( (tok = NextTok()) != T_EOF && tok != T_RIGHT ) { if( tok == T_LEFT ) tok = NextTok(); switch( tok ) { case T_version: NeedNUMBER( "a version number" ); Version = strtol( CurText(), NULL, 0 ); // How to report a problem that is about meaning rather than // syntax, and still get the file, line and column in the message. if( Version != 1 ) THROW_PARSE_ERROR( "only version 1 is supported", CurSource(), CurLine(), CurLineNumber(), CurOffset() ); NeedRIGHT(); break; case T_port: parsePort(); break; default: Unexpected( tok ); } } } protected: /** * Function parsePort * reads one: * (port (name "/dev/ttyS0") (baud 19200) (tag PUMP_PRESSURE) ) */ void parsePort() { PORT port; T tok; while( (tok = NextTok()) == T_LEFT ) { switch( (tok = NextTok()) ) { case T_name: // NeedSYMBOL() accepts a quoted string as well as a bare // symbol, so the file may write either form. NeedSYMBOL(); port.name = CurText(); break; case T_baud: NeedNUMBER( "a baud rate" ); port.baud = strtol( CurText(), NULL, 0 ); break; case T_tag: NeedSYMBOL(); port.tag = CurText(); break; default: Expecting( "name|baud|tag" ); } NeedRIGHT(); // close the (name ...) etc. } if( tok != T_RIGHT ) // close the (port ...) Expecting( T_RIGHT ); if( port.name.empty() ) THROW_PARSE_ERROR( "a port needs a (name ...)", CurSource(), CurLine(), CurLineNumber(), CurOffset() ); Ports.push_back( port ); } }; /** * Function ParseConfig * opens aFileName and runs the parser over it. */ static void ParseConfig( const char* aFileName ) { FILE* fp = fopen( aFileName, "rt" ); if( !fp ) { throw ERROR( E_NO_CONFIG_FILE, "unable to open configuration file '%s'", aFileName ); } Version = 0; Ports.clear(); // The lexer takes ownership of fp and closes it when it is destroyed, // including when a PARSE_ERROR unwinds out of Parse(). CONFIG_PARSER parser( fp, aFileName ); parser.Parse(); } void TLM::Init( req_init* req ) { printf( "%s: " __DATE__ " initializing...\n", Name() ); // MakeFilename() puts the configuration file beside the TLM itself, // i.e. /SoftPLC/tlm/SEXPRCFG.LST, wherever the TLM was loaded from. std::string filename = MakeFilename( CONFIG_FILE ); try { ParseConfig( filename.c_str() ); } catch( const PARSE_ERROR& pe ) { // A PARSE_ERROR already reads well; hand its text to the runtime as an // ERROR, which is the exception TLM::Init() is contracted to throw. throw ERROR( E_BAD_CONFIG_FILE, "configuration file error:\n %s", pe.Problem().c_str() ); } printf( "%s: version %d, %u port(s) configured:\n", Name(), Version, (unsigned) Ports.size() ); for( unsigned i = 0; i < Ports.size(); ++i ) { printf( "%s: %s at %d baud -> tag %s\n", Name(), Ports[i].name.c_str(), Ports[i].baud, Ports[i].tag.c_str() ); } } void TLM::DeInstall() { printf( "%s: deinstalling.\n", Name() ); Ports.clear(); }