--- linuxsampler/trunk/src/network/lscp.y 2010/10/04 12:20:23 2137 +++ linuxsampler/trunk/src/network/lscp.y 2014/02/05 20:45:18 2515 @@ -3,7 +3,7 @@ * LinuxSampler - modular, streaming capable sampler * * * * Copyright (C) 2003, 2004 by Benno Senoner and Christian Schoenebeck * - * Copyright (C) 2005 - 2010 Christian Schoenebeck * + * Copyright (C) 2005 - 2014 Christian Schoenebeck * * * * 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 * @@ -35,13 +35,15 @@ #include "lscpserver.h" #include "lscpevent.h" #include "lscpsymbols.h" +#include +#include "lscp.h" namespace LinuxSampler { // to save us typing work in the rules action definitions #define LSCPSERVER ((yyparse_param_t*) yyparse_param)->pServer #define SESSION_PARAM ((yyparse_param_t*) yyparse_param) -#define INCREMENT_LINE { SESSION_PARAM->iLine++; SESSION_PARAM->iColumn = 0; } +#define INCREMENT_LINE { SESSION_PARAM->iLine++; SESSION_PARAM->iColumn = 0; sParsed.clear(); } // clears input buffer void restart(yyparse_param_t* pparam, int& yychar); @@ -51,6 +53,7 @@ static int bytes = 0; // current number of characters in the input buffer static int ptr = 0; // current position in the input buffer static String sLastError; // error message of the last error occured +static String sParsed; ///< Characters of current line which have already been shifted (consumed/parsed) by the parser. // external reference to the function which actually reads from the socket extern int GetLSCPCommand( void *buf, int max_size); @@ -81,6 +84,7 @@ const char c = buf[ptr++]; // increment current reading position (just for verbosity / messages) GetCurrentYaccSession()->iColumn++; + sParsed += c; // we have to handle "normal" and "extended" ASCII characters separately if (isExtendedAsciiChar(c)) { // workaround for characters with ASCII code higher than 127 @@ -103,15 +107,76 @@ } -// we provide our own version of yyerror() so we don't have to link against the yacc library -void yyerror(const char* s); - using namespace LinuxSampler; +static std::set yyExpectedSymbols(); + +/** + * Will be called when an error occured (usually syntax error). + * + * We provide our own version of yyerror() so we a) don't have to link against + * the yacc library and b) can render more helpful syntax error messages. + */ +void yyerror(void* x, const char* s) { + yyparse_param_t* param = GetCurrentYaccSession(); + + // get the text part already parsed (of current line) + const bool bContainsLineFeed = + sParsed.find('\r') != std::string::npos || + sParsed.find('\n') != std::string::npos; + // remove potential line feed characters + if (bContainsLineFeed) { + for (size_t p = sParsed.find('\r'); p != std::string::npos; + p = sParsed.find('\r')) sParsed.erase(p); + for (size_t p = sParsed.find('\n'); p != std::string::npos; + p = sParsed.find('\n')) sParsed.erase(p); + } + + // start assembling the error message with Bison's own message + String txt = s; + + // append exact position info of syntax error + txt += (" (line:" + ToString(param->iLine+1)) + + (",column:" + ToString(param->iColumn)) + ")"; + + // append the part of the lined that has already been parsed + txt += ". Context: \"" + sParsed; + if (txt.empty() || bContainsLineFeed) + txt += "^"; + else + txt.insert(txt.size() - 1, "^"); + txt += "...\""; + + // append the non-terminal symbols expected now/next + std::set expectedSymbols = yyExpectedSymbols(); + for (std::set::const_iterator it = expectedSymbols.begin(); + it != expectedSymbols.end(); ++it) + { + if (it == expectedSymbols.begin()) + txt += " -> Should be: " + *it; + else + txt += " | " + *it; + } + + dmsg(2,("LSCPParser: %s\n", txt.c_str())); + sLastError = txt; +} + %} // reentrant parser -%pure_parser +%pure-parser + +%parse-param {void* yyparse_param} + +// After entering the yyparse() function, store references to the parser's +// state stack, so that we can create more helpful syntax error messages than +// Bison (2.x) could do. +%initial-action { + yyparse_param_t* p = (yyparse_param_t*) yyparse_param; + p->ppStackBottom = &yyss; + p->ppStackTop = &yyssp; +} // tell bison to spit out verbose syntax error messages %error-verbose @@ -187,6 +252,8 @@ ; add_instruction : CHANNEL { $$ = LSCPSERVER->AddChannel(); } + | CHANNEL SP MIDI_INPUT SP sampler_channel SP device_index { $$ = LSCPSERVER->AddChannelMidiInput($5,$7); } + | CHANNEL SP MIDI_INPUT SP sampler_channel SP device_index SP midi_input_port_index { $$ = LSCPSERVER->AddChannelMidiInput($5,$7,$9); } | DB_INSTRUMENT_DIRECTORY SP db_path { $$ = LSCPSERVER->AddDbInstrumentDirectory($3); } | DB_INSTRUMENTS SP NON_MODAL SP scan_mode SP db_path SP filename { $$ = LSCPSERVER->AddDbInstruments($5,$7,$9, true); } | DB_INSTRUMENTS SP NON_MODAL SP scan_mode SP FILE_AS_DIR SP db_path SP filename { $$ = LSCPSERVER->AddDbInstruments($5,$9,$11, true, true); } @@ -227,6 +294,10 @@ | TOTAL_STREAM_COUNT { $$ = LSCPSERVER->SubscribeNotification(LSCPEvent::event_total_stream_count); } | TOTAL_VOICE_COUNT { $$ = LSCPSERVER->SubscribeNotification(LSCPEvent::event_total_voice_count); } | GLOBAL_INFO { $$ = LSCPSERVER->SubscribeNotification(LSCPEvent::event_global_info); } + | EFFECT_INSTANCE_COUNT { $$ = LSCPSERVER->SubscribeNotification(LSCPEvent::event_fx_instance_count); } + | EFFECT_INSTANCE_INFO { $$ = LSCPSERVER->SubscribeNotification(LSCPEvent::event_fx_instance_info); } + | SEND_EFFECT_CHAIN_COUNT { $$ = LSCPSERVER->SubscribeNotification(LSCPEvent::event_send_fx_chain_count); } + | SEND_EFFECT_CHAIN_INFO { $$ = LSCPSERVER->SubscribeNotification(LSCPEvent::event_send_fx_chain_info); } ; unsubscribe_event : AUDIO_OUTPUT_DEVICE_COUNT { $$ = LSCPSERVER->UnsubscribeNotification(LSCPEvent::event_audio_device_count); } @@ -255,6 +326,10 @@ | TOTAL_STREAM_COUNT { $$ = LSCPSERVER->UnsubscribeNotification(LSCPEvent::event_total_stream_count); } | TOTAL_VOICE_COUNT { $$ = LSCPSERVER->UnsubscribeNotification(LSCPEvent::event_total_voice_count); } | GLOBAL_INFO { $$ = LSCPSERVER->UnsubscribeNotification(LSCPEvent::event_global_info); } + | EFFECT_INSTANCE_COUNT { $$ = LSCPSERVER->UnsubscribeNotification(LSCPEvent::event_fx_instance_count); } + | EFFECT_INSTANCE_INFO { $$ = LSCPSERVER->UnsubscribeNotification(LSCPEvent::event_fx_instance_info); } + | SEND_EFFECT_CHAIN_COUNT { $$ = LSCPSERVER->UnsubscribeNotification(LSCPEvent::event_send_fx_chain_count); } + | SEND_EFFECT_CHAIN_INFO { $$ = LSCPSERVER->UnsubscribeNotification(LSCPEvent::event_send_fx_chain_info); } ; map_instruction : MIDI_INSTRUMENT SP modal_arg midi_map SP midi_bank SP midi_prog SP engine_name SP filename SP instrument_index SP volume_value { $$ = LSCPSERVER->AddOrReplaceMIDIInstrumentMapping($4,$6,$8,$10,$12,$14,$16,MidiInstrumentMapper::DONTCARE,"",$3); } @@ -267,11 +342,14 @@ ; remove_instruction : CHANNEL SP sampler_channel { $$ = LSCPSERVER->RemoveChannel($3); } + | CHANNEL SP MIDI_INPUT SP sampler_channel { $$ = LSCPSERVER->RemoveChannelMidiInput($5); } + | CHANNEL SP MIDI_INPUT SP sampler_channel SP device_index { $$ = LSCPSERVER->RemoveChannelMidiInput($5,$7); } + | CHANNEL SP MIDI_INPUT SP sampler_channel SP device_index SP midi_input_port_index { $$ = LSCPSERVER->RemoveChannelMidiInput($5,$7,$9); } | MIDI_INSTRUMENT_MAP SP midi_map { $$ = LSCPSERVER->RemoveMidiInstrumentMap($3); } | MIDI_INSTRUMENT_MAP SP ALL { $$ = LSCPSERVER->RemoveAllMidiInstrumentMaps(); } | SEND_EFFECT_CHAIN SP device_index SP effect_chain { $$ = LSCPSERVER->RemoveSendEffectChain($3,$5); } | SEND_EFFECT_CHAIN SP EFFECT SP device_index SP effect_chain SP chain_pos { $$ = LSCPSERVER->RemoveSendEffectChainEffect($5,$7,$9); } - | FX_SEND SP SEND_EFFECT SP sampler_channel SP fx_send_id { $$ = LSCPSERVER->SetFxSendEffect($5,$7,-1,-1); } + | FX_SEND SP EFFECT SP sampler_channel SP fx_send_id { $$ = LSCPSERVER->SetFxSendEffect($5,$7,-1,-1); } | DB_INSTRUMENT_DIRECTORY SP FORCE SP db_path { $$ = LSCPSERVER->RemoveDbInstrumentDirectory($5, true); } | DB_INSTRUMENT_DIRECTORY SP db_path { $$ = LSCPSERVER->RemoveDbInstrumentDirectory($3); } | DB_INSTRUMENT SP db_path { $$ = LSCPSERVER->RemoveDbInstrument($3); } @@ -344,13 +422,14 @@ | FX_SEND SP AUDIO_OUTPUT_CHANNEL SP sampler_channel SP fx_send_id SP audio_channel_index SP audio_channel_index { $$ = LSCPSERVER->SetFxSendAudioOutputChannel($5,$7,$9,$11); } | FX_SEND SP MIDI_CONTROLLER SP sampler_channel SP fx_send_id SP midi_ctrl { $$ = LSCPSERVER->SetFxSendMidiController($5,$7,$9); } | FX_SEND SP LEVEL SP sampler_channel SP fx_send_id SP volume_value { $$ = LSCPSERVER->SetFxSendLevel($5,$7,$9); } - | FX_SEND SP SEND_EFFECT SP sampler_channel SP fx_send_id SP effect_chain SP chain_pos { $$ = LSCPSERVER->SetFxSendEffect($5,$7,$9,$11); } + | FX_SEND SP EFFECT SP sampler_channel SP fx_send_id SP effect_chain SP chain_pos { $$ = LSCPSERVER->SetFxSendEffect($5,$7,$9,$11); } | DB_INSTRUMENT_DIRECTORY SP NAME SP db_path SP stringval_escaped { $$ = LSCPSERVER->SetDbInstrumentDirectoryName($5,$7); } | DB_INSTRUMENT_DIRECTORY SP DESCRIPTION SP db_path SP stringval_escaped { $$ = LSCPSERVER->SetDbInstrumentDirectoryDescription($5,$7); } | DB_INSTRUMENT SP NAME SP db_path SP stringval_escaped { $$ = LSCPSERVER->SetDbInstrumentName($5,$7); } | DB_INSTRUMENT SP DESCRIPTION SP db_path SP stringval_escaped { $$ = LSCPSERVER->SetDbInstrumentDescription($5,$7); } | DB_INSTRUMENT SP FILE_PATH SP filename SP filename { $$ = LSCPSERVER->SetDbInstrumentFilePath($5,$7); } | ECHO SP boolean { $$ = LSCPSERVER->SetEcho((yyparse_param_t*) yyparse_param, $3); } + | SHELL SP INTERACT SP boolean { $$ = LSCPSERVER->SetShellInteract((yyparse_param_t*) yyparse_param, $5); } | VOLUME SP volume_value { $$ = LSCPSERVER->SetGlobalVolume($3); } | VOICES SP number { $$ = LSCPSERVER->SetGlobalMaxVoices($3); } | STREAMS SP number { $$ = LSCPSERVER->SetGlobalMaxStreams($3); } @@ -441,6 +520,7 @@ list_instruction : AUDIO_OUTPUT_DEVICES { $$ = LSCPSERVER->GetAudioOutputDevices(); } | MIDI_INPUT_DEVICES { $$ = LSCPSERVER->GetMidiInputDevices(); } | CHANNELS { $$ = LSCPSERVER->ListChannels(); } + | CHANNEL SP MIDI_INPUTS SP sampler_channel { $$ = LSCPSERVER->ListChannelMidiInputs($5); } | AVAILABLE_ENGINES { $$ = LSCPSERVER->ListAvailableEngines(); } | AVAILABLE_EFFECTS { $$ = LSCPSERVER->ListAvailableEffects(); } | EFFECT_INSTANCES { $$ = LSCPSERVER->ListEffectInstances(); } @@ -842,6 +922,12 @@ SET : 'S''E''T' ; +SHELL : 'S''H''E''L''L' + ; + +INTERACT : 'I''N''T''E''R''A''C''T' + ; + APPEND : 'A''P''P''E''N''D' ; @@ -950,6 +1036,18 @@ GLOBAL_INFO : 'G''L''O''B''A''L''_''I''N''F''O' ; +EFFECT_INSTANCE_COUNT : 'E''F''F''E''C''T''_''I''N''S''T''A''N''C''E''_''C''O''U''N''T' + ; + +EFFECT_INSTANCE_INFO : 'E''F''F''E''C''T''_''I''N''S''T''A''N''C''E''_''I''N''F''O' + ; + +SEND_EFFECT_CHAIN_COUNT : 'S''E''N''D''_''E''F''F''E''C''T''_''C''H''A''I''N''_''C''O''U''N''T' + ; + +SEND_EFFECT_CHAIN_INFO : 'S''E''N''D''_''E''F''F''E''C''T''_''C''H''A''I''N''_''I''N''F''O' + ; + INSTRUMENT : 'I''N''S''T''R''U''M''E''N''T' ; @@ -1010,9 +1108,6 @@ SEND_EFFECT_CHAIN : 'S''E''N''D''_''E''F''F''E''C''T''_''C''H''A''I''N' ; -SEND_EFFECT : 'S''E''N''D''_''E''F''F''E''C''T' - ; - SEND_EFFECT_CHAINS : 'S''E''N''D''_''E''F''F''E''C''T''_''C''H''A''I''N''S' ; @@ -1061,6 +1156,9 @@ MIDI_INPUT : 'M''I''D''I''_''I''N''P''U''T' ; +MIDI_INPUTS : 'M''I''D''I''_''I''N''P''U''T''S' + ; + MIDI_CONTROLLER : 'M''I''D''I''_''C''O''N''T''R''O''L''L''E''R' ; @@ -1171,21 +1269,256 @@ %% +#define DEBUG_BISON_SYNTAX_ERROR_WALKER 0 + /** - * Will be called when an error occured (usually syntax error). + * Internal function, only called by yyExpectedSymbols(). It is given a Bison + * parser state stack, reflecting the parser's entire state at a certain point, + * i.e. when a syntax error occured. This function will then walk ahead the + * potential parse tree starting from the current head of the given state + * stack. This function will call itself recursively to scan the individual + * parse tree branches. As soon as it hits on the next non-terminal grammar + * symbol in one parse tree branch, it adds the found non-terminal symbol to + * @a expectedSymbols and aborts scanning the respective tree branch further. + * If any local parser state is reached a second time, the respective parse + * tree is aborted to avoid any endless recursion. + * + * @param stack - Bison (yacc) state stack + * @param expectedSymbols - will be filled with next expected grammar symbols + * @param depth - just for internal debugging purposes + */ +static void walkAndFillExpectedSymbols(std::vector& stack, std::set& expectedSymbols, int depth = 0) { +#if DEBUG_BISON_SYNTAX_ERROR_WALKER + printf("\n"); + for (int i = 0; i < depth; ++i) printf("\t"); + printf("State stack:"); + for (int i = 0; i < stack.size(); ++i) { + printf(" %d", stack[i]); + } + printf("\n"); +#endif + + if (stack.empty()) return; + + int state = stack[stack.size() - 1]; + int n = yypact[state]; + if (n == YYPACT_NINF) { // default reduction required ... + // get default reduction rule for this state + n = yydefact[state]; + if (n <= 0 || n >= YYNRULES) return; // no rule, something is wrong + // return the new resolved expected symbol (left-hand symbol of grammar + // rule), then we're done in this state + expectedSymbols.insert(yytname[yyr1[n]]); + return; + } + if (!(YYPACT_NINF < n && n <= YYLAST)) return; + +#if DEBUG_BISON_SYNTAX_ERROR_WALKER + for (int i = 0; i < depth; ++i) printf("\t"); + printf("Expected tokens:"); +#endif + int begin = n < 0 ? -n : 0; + int checklim = YYLAST - n + 1; + int end = checklim < YYNTOKENS ? checklim : YYNTOKENS; + int rule, action, stackSize; + for (int token = begin; token < end; ++token) { + if (token == YYTERROR || yycheck[n + token] != token) continue; +#if DEBUG_BISON_SYNTAX_ERROR_WALKER + printf(" %s", yytname[token]); +#endif + + //if (yycheck[n + token] != token) goto default_reduction; + + action = yytable[n + token]; + if (action == 0 || action == YYTABLE_NINF) { +#if DEBUG_BISON_SYNTAX_ERROR_WALKER + printf(" (invalid action) "); fflush(stdout); +#endif + continue; // error, ignore + } + if (action < 0) { // reduction with rule -action required ... +#if DEBUG_BISON_SYNTAX_ERROR_WALKER + printf(" (reduction) "); fflush(stdout); +#endif + rule = -action; + goto reduce; + } + if (action == YYFINAL) continue; // "accept" state, we don't care about it here + + // "shift" required ... + + if (std::find(stack.begin(), stack.end(), action) != stack.end()) + continue; // duplicate state, ignore it to avoid endless recursions + + // "shift" / push the new state on the state stack and call this + // function recursively, and restore the stack after the recurse return + stackSize = stack.size(); + stack.push_back(action); + walkAndFillExpectedSymbols( //FIXME: could cause stack overflow (should be a loop instead), is probably fine with our current grammar though + stack, expectedSymbols, depth + 1 + ); + stack.resize(stackSize); // restore stack + continue; + + //default_reduction: // resolve default reduction for this state + // printf(" (default red.) "); fflush(stdout); + // rule = yydefact[state]; + + reduce: // "reduce" required +#if DEBUG_BISON_SYNTAX_ERROR_WALKER + printf(" (reduce by %d) ", rule); fflush(stdout); +#endif + if (rule == 0 || rule >= YYNRULES) continue; // invalid rule, something is wrong + // store the left-hand symbol of the grammar rule + expectedSymbols.insert(yytname[yyr1[rule]]); +#if DEBUG_BISON_SYNTAX_ERROR_WALKER + printf(" (SYM %s) ", yytname[yyr1[rule]]); fflush(stdout); +#endif + } +#if DEBUG_BISON_SYNTAX_ERROR_WALKER + printf("\n"); +#endif +} + +inline static int _yyReduce(std::vector& stack, const int& rule) { + if (stack.empty()) throw 1; // severe error + const int len = yyr2[rule]; + stack.resize(stack.size() - len); + YYTYPE_INT16 newState = yypgoto[yyr1[rule] - YYNTOKENS] + stack.back(); + if (0 <= newState && newState <= YYLAST && yycheck[newState] == stack.back()) + newState = yytable[newState]; + else + newState = yydefgoto[yyr1[rule] - YYNTOKENS]; + stack.push_back(newState); + return newState; +} + +inline static int _yyDefaultReduce(std::vector& stack) { + if (stack.empty()) throw 2; // severe error + int rule = yydefact[stack.back()]; + if (rule <= 0 || rule >= YYNRULES) throw 3; // no rule, something is wrong + return _yyReduce(stack, rule); +} + +#define DEBUG_PUSH_PARSE 0 + +static bool yyPushParse(std::vector& stack, char ch) { + startLabel: + +#if DEBUG_PUSH_PARSE + //printf("\n"); + //for (int i = 0; i < depth; ++i) printf("\t"); + printf("State stack:"); + for (int i = 0; i < stack.size(); ++i) { + printf(" %d", stack[i]); + } + printf(" char='%c'(%d)\n", ch, (int)ch); +#endif + + if (stack.empty()) return false; + + int state = stack.back(); + int n = yypact[state]; + if (n == YYPACT_NINF) { // default reduction required ... +#if DEBUG_PUSH_PARSE + printf("(def reduce 1)\n"); +#endif + state = _yyDefaultReduce(stack); + goto startLabel; + } + if (!(YYPACT_NINF < n && n <= YYLAST)) return false; + + YYTYPE_INT16 token = (ch == YYEOF) ? YYEOF : yytranslate[ch]; + n += token; + if (n < 0 || YYLAST < n || yycheck[n] != token) { +#if DEBUG_PUSH_PARSE + printf("(def reduce 2) n=%d token=%d\n", n, token); +#endif + state = _yyDefaultReduce(stack); + goto startLabel; + } + int action = yytable[n]; // yytable[yypact[state] + token] + if (action == 0 || action == YYTABLE_NINF) throw 4; + if (action < 0) { +#if DEBUG_PUSH_PARSE + printf("(reduce)\n"); +#endif + int rule = -action; + state = _yyReduce(stack, rule); + goto startLabel; + } + if (action == YYFINAL) return true; // final state reached + +#if DEBUG_PUSH_PARSE + printf("(push)\n"); +#endif + // push new state + state = action; + stack.push_back(state); + return true; +} + +static bool yyValid(std::vector& stack, char ch) { + try { + return yyPushParse(stack, ch); + } catch (int i) { +#if DEBUG_PUSH_PARSE + printf("exception %d\n", i); +#endif + return false; + } catch (...) { + return false; + } +} + +static int yyValidCharacters(std::vector& stack, const String& line) { + int i; + for (i = 0; i < line.size(); ++i) { + if (!yyValid(stack, line[i])) return i; + } + return i; +} + +/** + * Should only be called on syntax errors: returns a set of non-terminal + * symbols expected to appear now/next, just at the point where the syntax + * error appeared. */ -void yyerror(const char* s) { +static std::set yyExpectedSymbols() { + std::set result; yyparse_param_t* param = GetCurrentYaccSession(); - String msg = s - + (" (line:" + ToString(param->iLine+1)) - + ( ",column:" + ToString(param->iColumn)) - + ")"; - dmsg(2,("LSCPParser: %s\n", msg.c_str())); - sLastError = msg; + YYTYPE_INT16* ss = (*param->ppStackBottom); + YYTYPE_INT16* sp = (*param->ppStackTop); + int iStackSize = sp - ss + 1; + // copy and wrap parser's state stack into a convenient STL container + std::vector stack; + for (int i = 0; i < iStackSize; ++i) { + stack.push_back(ss[i]); + } + // do the actual parser work + walkAndFillExpectedSymbols(stack, result); + return result; } namespace LinuxSampler { +String lscpParserProcessShellInteraction(String& line, yyparse_param_t* param) { + std::vector stack; + stack.push_back(0); // every Bison symbol stack starts with zero + String l = line + '\n'; + int n = yyValidCharacters(stack, l); + String result = line; + result.insert(n <= result.length() ? n : result.length(), LSCP_SHK_GOOD_FRONT); + int code = (n > line.length()) ? LSCP_SHU_COMPLETE : (n < line.length()) ? + LSCP_SHU_SYNTAX_ERR : LSCP_SHU_INCOMPLETE; + result = "SHU:" + ToString(code) + ":" + result; + //if (n > line.length()) result += " [OK]"; +#if DEBUG_PUSH_PARSE + printf("%s\n", result.c_str()); +#endif + return result; +} + /** * Clears input buffer. */ @@ -1193,6 +1526,7 @@ bytes = 0; ptr = 0; sLastError = ""; + sParsed = ""; } }