2012-02-14 22 views
6

Estoy tratando de crear un analizador muy simple para una estructura tipo if-else que construirá y ejecutará una declaración SQL.Analizando el algoritmo if-else if statement

En lugar de probar las condiciones para ejecutar instrucciones, estaría probando las condiciones para construir una cadena.

Un ejemplo de un DECLARACIÓN sería:

select column1 
from 
#if(VariableA = Case1) 
table1 
#else if(VariableA = Case2) 
table2 
#else 
defaultTable 
#end 

Si variableA es igual Case1 la cadena resultante debe ser: select column1 from table1

Un ejemplo más complejo sería con if anidadas:

select column1 
from 
#if(VariableA = Case1) 
#if(VariableB = Case3) 
    table3 
#else 
    table4 
#else if(VariableA = Case2) 
table2 
#else 
defaultTable 
#end 

Aquí es donde realmente estoy teniendo problemas, no puedo pensar en una buena manera de identificar cada grupo if-else-end correctamente.

Además, no estoy seguro de qué manera de seguir si la cadena en la cláusula "else" debe evaluar como verdadera.

He estado buscando en la red diferentes tipos de algoritmos de análisis sintáctico, todos ellos muy abstractos y complejos.

¿Hay alguna sugerencia de un buen lugar para comenzar a estudiar ciencias no relacionadas con la informática?

+0

Espera, ¿estás analizando una cadena o construyendo una cadena? – Jay

+0

Estoy analizando una cadena Y construyendo una cadena. El resultado del análisis será una nueva cadena que contiene las partes deseadas de la consulta SQL. – ChandlerPelhams

+4

¿Hay alguna razón por la que está creando un idioma propio y no usa un idioma ya existente, como C#? – svick

Respuesta

8

Escribí un analizador simple, que probé en comparación con el ejemplo que proporcionó. Si desea saber más sobre el análisis, le sugiero que lea Compiler Construction de Niklaus Wirth.

El primer paso es escribir siempre la sintaxis de su idioma de forma adecuada. He elegido EBNF, que es muy simple de entender.

| separa las alternativas.

[ y ] adjuntar opciones.

{ y } denotan repeticiones (cero, uno o más).

( y ) expresiones de grupo (no se utiliza aquí).

Esta descripción no está completa pero el enlace que proporcioné la describe con más detalle.

La sintaxis EBNF

 
LineSequence = { TextLine | IfStatement }. 
TextLine  = <string>. 
IfStatement = IfLine LineSequence { ElseIfLine LineSequence } [ ElseLine LineSequence ] EndLine. 
IfLine  = "#if" "(" Condition ")". 
ElseLine  = "#else". 
ElseIfLine = "#else" "if" "(" Condition ")". 
EndLine  = "#end". 
Condition = Identifier "=" Identifier. 
Identifier = <letter_or_underline> { <letter_or_underline> | <digit> }. 

El analizador sigue de cerca la sintaxis, es decir, una repetición se traduce en un bucle, una alternativa en una instrucción if-else, y así sucesivamente.

using System; 
using System.Collections.Generic; 
using System.Text.RegularExpressions; 
using System.Windows.Forms; 

namespace Example.SqlPreprocessor 
{ 
    class Parser 
    { 
     enum Symbol 
     { 
      None, 
      LPar, 
      RPar, 
      Equals, 
      Text, 
      NumberIf, 
      If, 
      NumberElse, 
      NumberEnd, 
      Identifier 
     } 

     List<string> _input; // Raw SQL with preprocessor directives. 
     int _currentLineIndex = 0; 

     // Simulates variables used in conditions 
     Dictionary<string, string> _variableValues = new Dictionary<string, string> { 
      { "VariableA", "Case1" }, 
      { "VariableB", "CaseX" } 
     }; 

     Symbol _sy; // Current symbol. 
     string _string; // Identifier or text line; 
     Queue<string> _textQueue = new Queue<string>(); // Buffered text parts of a single line. 
     int _lineNo; // Current line number for error messages. 
     string _line; // Current line for error messages. 

     /// <summary> 
     /// Get the next line from the input. 
     /// </summary> 
     /// <returns>Input line or null if no more lines are available.</returns> 
     string GetLine() 
     { 
      if (_currentLineIndex >= _input.Count) { 
       return null; 
      } 
      _line = _input[_currentLineIndex++]; 
      _lineNo = _currentLineIndex; 
      return _line; 
     } 

     /// <summary> 
     /// Get the next symbol from the input stream and stores it in _sy. 
     /// </summary> 
     void GetSy() 
     { 
      string s; 
      if (_textQueue.Count > 0) { // Buffered text parts available, use one from these. 
       s = _textQueue.Dequeue(); 
       switch (s.ToLower()) { 
        case "(": 
         _sy = Symbol.LPar; 
         break; 
        case ")": 
         _sy = Symbol.RPar; 
         break; 
        case "=": 
         _sy = Symbol.Equals; 
         break; 
        case "if": 
         _sy = Symbol.If; 
         break; 
        default: 
         _sy = Symbol.Identifier; 
         _string = s; 
         break; 
       } 
       return; 
      } 

      // Get next line from input. 
      s = GetLine(); 
      if (s == null) { 
       _sy = Symbol.None; 
       return; 
      } 

      s = s.Trim(' ', '\t'); 
      if (s[0] == '#') { // We have a preprocessor directive. 
       // Split the line in order to be able get its symbols. 
       string[] parts = Regex.Split(s, @"\b|[^#_a-zA-Z0-9()=]"); 
       // parts[0] = # 
       // parts[1] = if, else, end 
       switch (parts[1].ToLower()) { 
        case "if": 
         _sy = Symbol.NumberIf; 
         break; 
        case "else": 
         _sy = Symbol.NumberElse; 
         break; 
        case "end": 
         _sy = Symbol.NumberEnd; 
         break; 
        default: 
         Error("Invalid symbol #{0}", parts[1]); 
         break; 
       } 

       // Store the remaining parts for later. 
       for (int i = 2; i < parts.Length; i++) { 
        string part = parts[i].Trim(' ', '\t'); 
        if (part != "") { 
         _textQueue.Enqueue(part); 
        } 
       } 
      } else { // We have an ordinary SQL text line. 
       _sy = Symbol.Text; 
       _string = s; 
      } 
     } 

     void Error(string message, params object[] args) 
     { 
      // Make sure parsing stops here 
      _sy = Symbol.None; 
      _textQueue.Clear(); 
      _input.Clear(); 

      message = String.Format(message, args) + 
         String.Format(" in line {0}\r\n\r\n{1}", _lineNo, _line); 
      Output("------"); 
      Output(message); 
      MessageBox.Show(message, "Error"); 
     } 

     /// <summary> 
     /// Writes the processed line to a (simulated) output stream. 
     /// </summary> 
     /// <param name="line">Line to be written to output</param> 
     void Output(string line) 
     { 
      Console.WriteLine(line); 
     } 

     /// <summary> 
     /// Starts the parsing process. 
     /// </summary> 
     public void Parse() 
     { 
      // Simulate an input stream. 
      _input = new List<string> { 
       "select column1", 
       "from", 
       "#if(VariableA = Case1)", 
       " #if(VariableB = Case3)", 
       "  table3", 
       " #else", 
       "  table4", 
       " #end", 
       "#else if(VariableA = Case2)", 
       " table2", 
       "#else", 
       " defaultTable", 
       "#end" 
      }; 

      // Clear previous parsing 
      _textQueue.Clear(); 
      _currentLineIndex = 0; 

      // Get first symbol and start parsing 
      GetSy(); 
      if (LineSequence(true)) { // Finished parsing successfully. 
       //TODO: Do something with the generated SQL 
      } else { // Error encountered. 
       Output("*** ABORTED ***"); 
      } 
     } 

     // The following methods parse according the the EBNF syntax. 

     bool LineSequence(bool writeOutput) 
     { 
      // EBNF: LineSequence = { TextLine | IfStatement }. 
      while (_sy == Symbol.Text || _sy == Symbol.NumberIf) { 
       if (_sy == Symbol.Text) { 
        if (!TextLine(writeOutput)) { 
         return false; 
        } 
       } else { // _sy == Symbol.NumberIf 
        if (!IfStatement(writeOutput)) { 
         return false; 
        } 
       } 
      } 
      return true; 
     } 

     bool TextLine(bool writeOutput) 
     { 
      // EBNF: TextLine = <string>. 
      if (writeOutput) { 
       Output(_string); 
      } 
      GetSy(); 
      return true; 
     } 

     bool IfStatement(bool writeOutput) 
     { 
      // EBNF: IfStatement = IfLine LineSequence { ElseIfLine LineSequence } [ ElseLine LineSequence ] EndLine. 
      bool result; 
      if (IfLine(out result) && LineSequence(writeOutput && result)) { 
       writeOutput &= !result; // Only one section can produce an output. 
       while (_sy == Symbol.NumberElse) { 
        GetSy(); 
        if (_sy == Symbol.If) { // We have an #else if 
         if (!ElseIfLine(out result)) { 
          return false; 
         } 
         if (!LineSequence(writeOutput && result)) { 
          return false; 
         } 
         writeOutput &= !result; // Only one section can produce an output. 
        } else { // We have a simple #else 
         if (!LineSequence(writeOutput)) { 
          return false; 
         } 
         break; // We can have only one #else statement. 
        } 
       } 
       if (_sy != Symbol.NumberEnd) { 
        Error("'#end' expected"); 
        return false; 
       } 
       GetSy(); 
       return true; 
      } 
      return false; 
     } 

     bool IfLine(out bool result) 
     { 
      // EBNF: IfLine = "#if" "(" Condition ")". 
      result = false; 
      GetSy(); 
      if (_sy != Symbol.LPar) { 
       Error("'(' expected"); 
       return false; 
      } 
      GetSy(); 
      if (!Condition(out result)) { 
       return false; 
      } 
      if (_sy != Symbol.RPar) { 
       Error("')' expected"); 
       return false; 
      } 
      GetSy(); 
      return true; 
     } 

     private bool Condition(out bool result) 
     { 
      // EBNF: Condition = Identifier "=" Identifier. 
      string variable; 
      string expectedValue; 
      string variableValue; 

      result = false; 
      // Identifier "=" Identifier 
      if (_sy != Symbol.Identifier) { 
       Error("Identifier expected"); 
       return false; 
      } 
      variable = _string; // The first identifier is a variable. 
      GetSy(); 
      if (_sy != Symbol.Equals) { 
       Error("'=' expected"); 
       return false; 
      } 
      GetSy(); 
      if (_sy != Symbol.Identifier) { 
       Error("Value expected"); 
       return false; 
      } 
      expectedValue = _string; // The second identifier is a value. 

      // Search the variable 
      if (_variableValues.TryGetValue(variable, out variableValue)) { 
       result = variableValue == expectedValue; // Perform the comparison. 
      } else { 
       Error("Variable '{0}' not found", variable); 
       return false; 
      } 

      GetSy(); 
      return true; 
     } 

     bool ElseIfLine(out bool result) 
     { 
      // EBNF: ElseIfLine = "#else" "if" "(" Condition ")". 
      result = false; 
      GetSy(); // "#else" already processed here, we are only called if the symbol is "if" 
      if (_sy != Symbol.LPar) { 
       Error("'(' expected"); 
       return false; 
      } 
      GetSy(); 
      if (!Condition(out result)) { 
       return false; 
      } 
      if (_sy != Symbol.RPar) { 
       Error("')' expected"); 
       return false; 
      } 
      GetSy(); 
      return true; 
     } 
    } 
} 

Tenga en cuenta que el anidado sentencias if se procesan automáticamente de una manera muy natural. Primero, la gramática se expresa recursivamente. Un LineSequence puede contener IfStatment sy IfStatment s contienen LineSequence s. En segundo lugar, esto da como resultado métodos de procesamiento de sintaxis que se llaman recíprocamente. La anidación de elementos de sintaxis se traduce, por lo tanto, en llamadas a métodos recursivos.

+0

<3 Olivier, ¡muchas gracias por una solución tan completa e informativa! – ChandlerPelhams

3

Tome un vistazo a Irony:

ironía es un kit de desarrollo para la implementación de idiomas en la plataforma .NET . A diferencia de la mayoría de las soluciones yacc/lex existentes, Irony no utiliza ninguna generación de código de analizador o analizador a partir de las especificaciones de gramática escritas en un metalenguaje especializado. En Irony, la gramática del idioma de destino está codificada directamente en C# utilizando la sobrecarga del operador para expresar construcciones gramaticales. Los módulos de escáner y analizador de Irony usan la gramática codificada como clase C# para controlar el proceso de análisis . Consulte la muestra de gramática de expresión para ver un ejemplo de la definición de gramática en la clase C# y utilícela en un analizador en funcionamiento.

1

Te recomiendo que uses un generador de código existente como ... plantillas C# o T4 o vistas parciales ASP.NET MVC.

Pero si quieres hacer esto tú mismo, necesitas algún tipo de recursión (o una pila que sea equivalente). Podría funcionar así:

string BuildCode(string str) 
{ 
foreach(Match ifMatch in Regex.Matches("#if(?<condition>[^\n\r]*)[\r\n]*(?<body>.*?)#endif) 
{ 
    var condition = ifMatch.Groups["condition"].Value; 
    return EvaluateCondition(condition) ? BuildCode(ifMatch.Value) : null; 
} 
} 

Esto es un pseudocódigo. Necesitas pensar en esto tú mismo. Esto tampoco es compatible con una rama else pero puede agregarlo fácilmente.

Aquí hay una nueva respuesta: use CodeDom para compilar una función C#. Puede usar la potencia total de C# pero tener el código de C# almacenado en una base de datos más o menos. De esta manera no tienes que volver a desplegar.

+1

Esa expresión regular no tuvo en cuenta la instrucción else. Si todas sus condiciones tienen la forma x = y, en C# .NET, más fácil de usar por primera vez: Regex ifElse = new Regex (@ "# if \ s * [(] \ s * (? [^ =)] *) \ s * = \ s * (? [^)] *) \ s * [)] (? [^ #] *) (? (?: # else si [^ #] *) *) (?: # else (? [^ #] *))? (?: # end) "). Luego, procese el "else ifs" con una expresión regular similar para #else if, y emita una instrucción .Matches simple: devolverá la matriz de resultados, donde cada entrada es una matriz que contiene la "coincidencia completa" y la coincidencia grupos (es decir, variable, valor, etc.). –