di fronte ad una situazione simile - la necessità di gestire brevi espressioni di una sola riga - ho scritto un parser. Le espressioni erano la logica booleana, nella forma
n1 = y and n2 > z
n2 != x or (n3 > y and n4 = z)
e così via. In inglese si potrebbe dire che ci sono atomi uniti da AND e OR, e ogni atomo ha tre elementi: un attributo sul lato sinistro, un operatore e un valore. Perché era così succinta che penso che l'analisi fosse più facile. L'insieme dei possibili attributi è noto e limitato (ad esempio: nome, dimensione, tempo).Gli operatori variano in base agli attributi: attributi diversi richiedono set di operatori diversi. E la gamma e il formato dei valori possibili variano a seconda dell'attributo.
Per analizzare, ho diviso la stringa su spazio vuoto usando String.Split(). In seguito mi sono reso conto che prima di Split(), avevo bisogno di normalizzare la stringa di input, inserendo spazi bianchi prima e dopo il paren. L'ho fatto con una regex.Replace().
L'output della divisione è una serie di token. Quindi l'analisi si verifica in un ciclo di grandi dimensioni con un interruttore sul valore dell'attributo sul lato sinistro. Con ogni giro del ciclo, ero pronto a borbottare in un gruppo di gettoni. Se il primo token era un open paren, allora il gruppo era solo un token di lunghezza: il paren stesso. Per i token che erano nomi noti - i miei valori di attributo - il parser doveva borbottare in un gruppo di 3 token, uno per nome, operatore e valore. Se in qualsiasi momento non ci sono abbastanza token, il parser genera un'eccezione. Basato sul flusso di token, lo stato del parser cambierebbe. Una congiunzione (AND, OR, XOR) significava spingere l'atomo precedente su una pila, e quando l'atomo successivo era finito, io avrei fatto scoppiare l'atomo precedente e unire quei due atomi in un atomo composto. E così via. La gestione dello stato si è verificata alla fine di ogni ciclo del parser.
Atom current;
for (int i=0; i < tokens.Length; i++)
{
switch (tokens[i].ToLower())
{
case "name":
if (tokens.Length <= i + 2)
throw new ArgumentException();
Comparison o = (Comparison) EnumUtil.Parse(typeof(Comparison), tokens[i+1]);
current = new NameAtom { Operator = o, Value = tokens[i+2] };
i+=2;
stateStack.Push(ParseState.AtomDone);
break;
case "and":
case "or":
if (tokens.Length <= i + 3)
throw new ArgumentException();
pendingConjunction = (LogicalConjunction)Enum.Parse(typeof(LogicalConjunction), tokens[i].ToUpper());
current = new CompoundAtom { Left = current, Right = null, Conjunction = pendingConjunction };
atomStack.Push(current);
break;
case "(":
state = stateStack.Peek();
if (state != ParseState.Start && state != ParseState.ConjunctionPending && state != ParseState.OpenParen)
throw new ArgumentException();
if (tokens.Length <= i + 4)
throw new ArgumentException();
stateStack.Push(ParseState.OpenParen);
break;
case ")":
state = stateStack.Pop();
if (stateStack.Peek() != ParseState.OpenParen)
throw new ArgumentException();
stateStack.Pop();
stateStack.Push(ParseState.AtomDone);
break;
// more like that...
case "":
// do nothing in the case of whitespace
break;
default:
throw new ArgumentException(tokens[i]);
}
// insert housekeeping for parse states here
}
Questo è semplificato, solo un po '. Ma l'idea è che ogni dichiarazione del caso è abbastanza semplice. È facile analizzare in un'unità atomica dell'espressione. La parte difficile era unirli tutti insieme in modo appropriato.
Questo trucco è stato eseguito nella sezione di pulizia, alla fine di ogni loop di slurp, utilizzando la pila di stati e la pila di atomi. Cose diverse possono accadere in base allo stato del parser. Come ho detto, in ogni dichiarazione di caso, lo stato del parser potrebbe cambiare, con lo stato precedente essere spinto su una pila. Poi alla fine dell'istruzione switch, se lo stato diceva che avevo appena finito di analizzare un atomo e c'era una congiunzione in sospeso, avrei spostato l'atomo appena analizzato nel CompoundAtom. Il codice è simile al seguente:
state = stateStack.Peek();
if (state == ParseState.AtomDone)
{
stateStack.Pop();
if (stateStack.Peek() == ParseState.ConjunctionPending)
{
while (stateStack.Peek() == ParseState.ConjunctionPending)
{
var cc = critStack.Pop() as CompoundAtom;
cc.Right = current;
current = cc; // mark the parent as current (walk up the tree)
stateStack.Pop(); // the conjunction is no longer pending
state = stateStack.Pop();
if (state != ParseState.AtomDone)
throw new ArgumentException();
}
}
else stateStack.Push(ParseState.AtomDone);
}
Un altro po 'di magia era EnumUtil.Parse. Ciò mi consente di analizzare cose come "<" in un valore enum. Si supponga di definire le enumerazioni in questo modo:
internal enum Operator
{
[Description(">")] GreaterThan,
[Description(">=")] GreaterThanOrEqualTo,
[Description("<")] LesserThan,
[Description("<=")] LesserThanOrEqualTo,
[Description("=")] EqualTo,
[Description("!=")] NotEqualTo
}
Normalmente Enum.Parse cerca il nome simbolico del valore enum, e < non è un nome simbolico valido. EnumUtil.Parse() cerca la cosa nella descrizione. Il codice è simile al seguente:
internal sealed class EnumUtil
{
/// <summary>
/// Returns the value of the DescriptionAttribute if the specified Enum value has one.
/// If not, returns the ToString() representation of the Enum value.
/// </summary>
/// <param name="value">The Enum to get the description for</param>
/// <returns></returns>
internal static string GetDescription(System.Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes.Length > 0)
return attributes[0].Description;
else
return value.ToString();
}
/// <summary>
/// Converts the string representation of the name or numeric value of one or more enumerated constants to an equivilant enumerated object.
/// Note: Utilised the DescriptionAttribute for values that use it.
/// </summary>
/// <param name="enumType">The System.Type of the enumeration.</param>
/// <param name="value">A string containing the name or value to convert.</param>
/// <returns></returns>
internal static object Parse(Type enumType, string value)
{
return Parse(enumType, value, false);
}
/// <summary>
/// Converts the string representation of the name or numeric value of one or more enumerated constants to an equivilant enumerated object.
/// A parameter specified whether the operation is case-sensitive.
/// Note: Utilised the DescriptionAttribute for values that use it.
/// </summary>
/// <param name="enumType">The System.Type of the enumeration.</param>
/// <param name="value">A string containing the name or value to convert.</param>
/// <param name="ignoreCase">Whether the operation is case-sensitive or not.</param>
/// <returns></returns>
internal static object Parse(Type enumType, string stringValue, bool ignoreCase)
{
if (ignoreCase)
stringValue = stringValue.ToLower();
foreach (System.Enum enumVal in System.Enum.GetValues(enumType))
{
string description = GetDescription(enumVal);
if (ignoreCase)
description = description.ToLower();
if (description == stringValue)
return enumVal;
}
return System.Enum.Parse(enumType, stringValue, ignoreCase);
}
}
Ho ricevuto quella cosa EnumUtil.Parse() da un'altra parte. Forse qui?
Vuoi compilare il codice per il CLR? Sembra un obiettivo ragionevole per una DSL. – MSalters
Questo è un compito abbastanza difficile (per interpretare questi tipi di funzioni). Targeting del CLR non renderà tutto più semplice ... –
possibile duplicato di [Imparare a scrivere un compilatore] (http://stackoverflow.com/questions/1669/learning-to-write-a-compiler) –