写一个迷你语言

时间:2009-03-05 14:40:30

标签: .net language-design

我有一个应用程序,需要允许用户编写类似于excel的表达式:

(H1 +(D1 / C3))* I8

和更复杂的事情,如

如果(H1 ='真',D3 * .2,D3 * .5)

我只能用正则表达式做这么多。关于这样做的正确方法以及我可以学习的任何资源的任何建议都将不胜感激。

谢谢!

9 个答案:

答案 0 :(得分:7)

其他一些问题,你会在以下网址找到提示:

祝你好运!

答案 1 :(得分:7)

当遇到类似的情况 - 需要处理短的单行表达式时 - 我写了一个解析器。表达式是布尔逻辑,格式为

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

等等。在英语中你可以说有AND和OR连接的原子,每个原子有三个元素 - 一个左侧属性,一个运算符和一个值。因为它是如此的succint我认为解析更容易。可能属性的集合是已知且有限的(例如:名称,大小,时间)。运算符因属性而异:不同的属性采用不同的运算符集。并且可能值的范围和格式也根据属性而变化。

要解析,我使用String.Split()在空格上拆分字符串。 我后来意识到在Split()之前,我需要规范化输入字符串 - 在parens之前和之后插入空格。我用regex.Replace()做到了这一点。

分割的输出是一个标记数组。然后解析发生在一个大的for循环中,左侧属性值上有一个开关。随着循环的每次循环,我被设置为在一组令牌中啜饮。如果第一个令牌是开放式的,那么该组只有一个令牌:paren本身。对于众所周知的名称 - 我的属性值 - 解析器必须在一组3个令牌中啜饮,每个令牌对应于名称,运算符和值。如果在任何时候没有足够的令牌,解析器会抛出异常。基于令牌流,解析器状态将发生变化。连接(AND,OR,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

}

这简化了,只是一点点。但这个想法是每个案例陈述都相当简单。在表达式的原子单元中解析很容易。棘手的部分是将它们恰当地加在一起。

使用状态堆栈和原子堆栈,在每个slurp循环结束时的内务部分完成了这个技巧。根据解析器状态可能会发生不同的事情。正如我所说的,在每个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。这允许我解析像“&lt;”这样的东西进入枚举值。假设您定义了这样的枚举:

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

通常,Enum.Parse会查找枚举值的符号名称,并且&lt;不是有效的符号名称。 EnumUtil.Parse()在描述中查找事物。代码如下所示:

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()的东西。也许在这里?

答案 2 :(得分:4)

一个小的递归下降解析器是完美的。您甚至可能不需要构建解析树 - 您可以在解析时进行评估。

 /* 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宏的原因是为了更容易在中间优先级添加运算符。

执行“if”语句更复杂一些。每个解析例程都需要一个额外的“启用”参数,因此除非启用它,否则它不会进行计算。然后解析单词“if”,解析测试表达式,然后解析两个结果表达式,禁用非活动表达式。

答案 3 :(得分:3)

您可以使用.NET JScript编译器,或与IronPython,IronRuby或IronScheme接口(按字母顺序命名,而不是首选项; p)。

答案 4 :(得分:2)

我有一个例子这样做:Will o’ the Wisp(因为这是我自己的代码,我有信心批评它。)

关于守则的是什么?

  1. 因此使用了一种设计模式:解释器模式
  2. 设计相当简洁
  3. 它以一种很好的方式使用属性。
  4. 它产生漂亮的图形。 ; - )
  5. Turtle graphics http://i3.codeplex.com/Project/Download/FileDownload.aspx?ProjectName=wisp&DownloadId=34823

    关于代码的错误是什么?

    1. 这很慢
    2. 关于列表(数据与代码),语言定义不明确。

答案 5 :(得分:2)

结帐ANTLR。您可以定义语言语法,使用GUI工具对其进行测试,并生成各种语言的源代码。开源。

答案 6 :(得分:2)

我会推荐这本书Constructing Little Languages。它将指导您完成正确完成此任务所需的许多编译器基础知识。

你提出这样一个事实,即除非你对你的语言有一些严格的限制,否则正则表达式将无效。就像其他人所说的那样,Recursive Descent Parser会做到这一点。

接下来的选择是使用Parser Generator ANTLR,还是从头开始编写。

答案 7 :(得分:1)

看看这个开源项目:

Excel Financial Functions

答案 8 :(得分:0)

我建议看看CoreCalc / FunCalc的工作: http://www.itu.dk/people/sestoft/funcalc/

我在生产中使用他们的COCO \ R解析器生成器的语法,它的工作非常快。

您需要做的就是: 1.从corecalc获得excel语法 2.在其上运行coco.exe(为excel类表达式生成解析器) 3.翻译表达式树以反转波兰表示法 4.简单计算