Написание мини-языка - PullRequest
       17

Написание мини-языка

7 голосов
/ 05 марта 2009

У меня есть приложение, которое должно позволять пользователям писать выражения, аналогичные Excel:

(H1 + (D1 / C3)) * I8

и более сложные вещи, такие как

Если (H1 = «Истина», D3 * .2, D3 * .5)

Я могу делать так много только с регулярными выражениями. Будем весьма благодарны за любые предложения относительно правильного подхода к этому, а также любые ресурсы, из которых я могу извлечь уроки.

Спасибо!

Ответы [ 9 ]

7 голосов
/ 05 марта 2009

Столкнувшись с аналогичной ситуацией - необходимостью обрабатывать короткие однострочные выражения - я написал парсер. Выражения были булевой логики вида

n1 = y and n2 > z
n2 != x or (n3 > y and n4 = z) 

и так далее. По-английски можно сказать, что есть атомы, соединенные AND и OR, и каждый атом имеет три элемента - атрибут левой части, оператор и значение. Потому что это было так лаконично, я думаю, что анализ был легче. Набор возможных атрибутов известен и ограничен (например, имя, размер, время). Операторы различаются по атрибутам: разные атрибуты принимают разные наборы операторов. И диапазон и формат возможных значений также варьируются в зависимости от атрибута.

Для разбора я разбил строку на пробел, используя String.Split (). Позже я понял, что до Split () мне нужно было нормализовать входную строку - вставлять пробелы до и после скобок. Я сделал это с помощью регулярного выражения. Replace ().

Результатом разбиения является массив токенов. Затем выполняется синтаксический анализ в большом цикле for с переключателем на левом значении атрибута. С каждым циклом цикла я собирался выпить группу жетонов. Если первый токен был открытым пареном, то у группы был всего один токен в длину: сам парен. Для токенов, которые были общеизвестными именами - мои значения атрибутов - парсер должен был отбросить группу из 3 токенов, по одному для имени, оператора и значения. Если в какой-то момент токенов недостаточно, парсер выдает исключение. В зависимости от потока токенов состояние синтаксического анализатора изменится. Соединение (И, ИЛИ, XOR) предназначено для помещения предыдущего атома в стек, и когда следующий атом будет завершен, я добавлю предыдущий атом и соединю эти два атома в составной атом. И так далее. Управление состоянием происходило в конце каждого цикла парсера.

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

}

Это немного упрощено. Но идея состоит в том, что каждое утверждение случая довольно просто. Это легко разобрать в атомарной единице выражения. Сложная часть соединяла их всех вместе соответствующим образом.

Этот трюк был выполнен в служебной части, в конце каждого цикла, используя стек состояний и стек атомов. В зависимости от состояния парсера могут происходить разные вещи. Как я уже говорил, в каждом операторе case состояние анализатора может меняться, а предыдущее состояние помещается в стек. Затем, в конце оператора switch, если государство сообщило, что я только что закончил синтаксический анализ атома, и было ожидающее соединение, я переместил только что проанализированный атом в CompoundAtom. Код выглядит так:

            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); 
            }

Еще одна магия - EnumUtil.Parse. Это позволяет мне разбирать такие вещи, как «<», в значение enum. Предположим, вы определили свои перечисления следующим образом: </p>

internal enum Operator
{
    [Description(">")]   GreaterThan,
    [Description(">=")]  GreaterThanOrEqualTo,
    [Description("<")]   LesserThan,
    [Description("<=")]  LesserThanOrEqualTo,
    [Description("=")]   EqualTo,
    [Description("!=")]  NotEqualTo
}

Обычно Enum.Parse ищет символическое имя значения enum, а <не является допустимым символическим именем. EnumUtil.Parse () ищет вещь в описании. Код выглядит так: </p>

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);
    }

}

Я получил эту вещь EnumUtil.Parse () откуда-то еще. Может быть здесь?

7 голосов
/ 05 марта 2009
4 голосов
/ 05 марта 2009

Небольшой парсер рекурсивного спуска идеально подходит для этого. Возможно, вам даже не нужно создавать дерево разбора - вы можете выполнять оценку во время разбора.

 /* here's a teeny one in C++ */
void ScanWhite(const char* &p){
  while (*p==' ') p++;
}

bool ParseNum(const char* &p, double &v){
  ScanWhite(p);
  if (!DIGIT(*p)) return false;
  const char* p0 = p;
  while(DIGIT(*p)) p++;
  if (*p == '.'){
    p++;
    while(DIGIT(*p)) p++;
  }
  v = /* value of characters p0 up to p */;
  return true;
}

bool ParseId(const char* &p, double &v){
  ScanWhite(p);
  if (ALPHA(p[0]) && DIGIT(p[1])){
    v = /* value of cell whose name is p[0], p[1] */;
    p += 2;
    return true;
  }
  return false;
}

bool ParseChar(const char* &p, char c){
  ScanWhite(p);
  if (*p != c) return false;
  p++;
  return true;
}

void ParseExpr(const char* &p, double &v); /* forward declaration */

void ParsePrimitive(const char* &p, double &v){
  if (ParseNum(p, v));
  else if (ParseId(p, v));
  else if (ParseChar(p, '(')){
    ParseExpr(p, v);
    if (!ParseChar(p, ')'){/* throw syntax error */}
  }
  else {/* throw syntax error */}
}
#define PARSE_HIGHER ParsePrimitive

void ParseUnary(const char* &p, double &v){
  if (ParseChar(p, '-')){
    ParseUnary(p, v);
    v = -v;
  }
  else {
    PARSE_HIGHER(p, v);
  }
}
#undef  PARSE_HIGHER
#define PARSE_HIGHER ParseUnary

void ParseProduct(const char* &p, double &v){
  double v2;
  PARSE_HIGHER(p, v);
  while(true){
    if (ParseChar(p, '*')){
      PARSE_HIGHER(p, v2);
      v *= v2;
    }
    else if (ParseChar(p, '/')){
      PARSE_HIGHER(p, v2);
      v /= v2;
    }
    else break;
  }
}
#undef  PARSE_HIGHER
#define PARSE_HIGHER ParseProduct

void ParseSum(const char* &p, double &v){
  double v2;
  PARSE_HIGHER(p, v);
  while(true){
    if (ParseChar(p, '+')){
      PARSE_HIGHER(p, v2);
      v += v2;
    }
    else if (ParseChar(p, '-')){
      PARSE_HIGHER(p, v2);
      v -= v2;
    }
    else break;
  }
}
#undef  PARSE_HIGHER
#define PARSE_HIGHER ParseSum

void ParseExpr(const char* &p, double &v){
  PARSE_HIGHER(p, v);
}

double ParseTopLevel(const char* buf){
  const char* p = buf;
  double v;
  ParseExpr(p, v);
  return v;
}

Теперь, если вы просто вызовете ParseTop, он вычислит значение выражения для вас.

Причина макроса PARSE_HIGHER состоит в том, чтобы упростить добавление операторов на промежуточных уровнях приоритета.

Чтобы сделать утверждение "если", это немного сложнее. Каждой процедуре синтаксического анализа требуется дополнительный аргумент «enable», поэтому она не выполняет вычислений, если она не включена. Затем вы анализируете слово «if», анализируете тестовое выражение, а затем анализируете два результирующих выражения, причем неактивное отключено.

3 голосов
/ 05 марта 2009

Вы можете использовать компилятор .NET JScript или взаимодействовать с IronPython, IronRuby или IronScheme (по алфавиту, без предпочтения; p).

2 голосов
/ 05 марта 2009

Я бы порекомендовал книгу Создание маленьких языков . Он проведет вас через многие основы компилятора, необходимые для правильного выполнения этой задачи.

Вы упомянули тот факт, что регулярные выражения не будут работать, если у вас нет строгих ограничений на ваш язык. Как уже говорили другие, парсер рекурсивного спуска сделает свое дело.

Следующий выбор - использовать Parser Generator , например ANTLR , или написать его с нуля.

2 голосов
/ 05 марта 2009

Выезд ANTLR . Вы определяете синтаксис языка, тестируете его с помощью инструмента с графическим интерфейсом и генерируете исходный код на разных языках. Открытый исходный код.

2 голосов
/ 05 марта 2009

У меня есть контр-пример того, как , а не , чтобы это сделать: Будет ли Wisp (так как это мой собственный код, я чувствую себя уверенно критикуя его).

Что хорошо в коде?

  1. Следовательно, он использует шаблон проектирования: Шаблон интерпретатора
  2. имеет довольно чистый дизайн
  3. Хорошо использует атрибуты.
  4. Это производит хорошую графику. ; -)

Черепаха графика http://i3.codeplex.com/Project/Download/FileDownload.aspx?ProjectName=wisp&DownloadId=34823

Что плохо в коде?

  1. Это медленно !
  2. Язык плохо определен в отношении списков (данные и код).
1 голос
/ 05 марта 2009

Посмотрите на этот проект с открытым исходным кодом:

Финансовые функции Excel

0 голосов
/ 18 мая 2012

Рекомендую посмотреть на работу CoreCalc / FunCalc: http://www.itu.dk/people/sestoft/funcalc/

Я использовал их грамматику для генератора парсеров COCO \ R, и он работает очень быстро.

Все, что вам нужно сделать, это: 1. получить грамматику Excel от corecalc 2. запустите на нем программу coco.exe (генерирует синтаксический анализатор для выражений, подобных Excel) 3. перевести дерево выражений в обратную польскую запись 4. простой расчет

...