如何在C#中评估和处理简单的字符串语法树?

时间:2011-03-22 18:03:53

标签: c# parsing antlr abstract-syntax-tree

我有一个基于令牌索引的文档语料库,它提供了一种查询方法。用户手动(!)输入需要解析和评估的查询字符串。然后,语料库应返回与给定查询字符串匹配的所有文档的列表。查询语言具有简单的布尔运算符AND,NOT和OR,它们也可以通过括号进行优先级排序。 经过一些研究后,我已经使用ANTLR将给定的查询字符串解析为语法树。

例如:查询

"Bill OR (John AND Jim) OR (NOT Simon AND Mike)"

在以下语法树中翻译:

编辑:请参阅Bart Kiers帖子中的正确图表(复制到此处):

enter image description here

树中的所有节点都是简单的字符串,每个节点都知道它的父节点和子节点,但不知道它的兄弟节点。 正如您所看到的,ANTLR语法已经规定了操作需要执行的顺序:树底部的那些首先出现。

所以我可能需要做的是反复(?)评估树中的所有操作数。 通常,我可以使用树中每个叶子的方法Get(字符串术语)对我的语料库进行简单搜索(如“Bill”或“John”)。 Get()返回包含叶子中术语的文档列表。我还可以评估每个叶子的父级以识别一个可能的NOT运算符,然后该运算符将导致不包含叶子中的术语的文档的结果列表(使用方法Not()而不是Get())。

AND和OR运算符应转换为需要两个参数的方法调用:

  • AND应该调用一个方法Intersect(list1,list2),它返回list1和list2中的文档列表。
  • OR应该调用一个方法Union(list1,list2),它返回list1或list2中的文档列表。

参数list1和list2包含我在使用Get()或Not()之前收到的文档。

我的问题是:我如何 - 在C#语义和语法上 - 评估所有必要的搜索术语并使用它们以正确的顺序调用正确的运算符方法?直觉上它听起来像递归但不知何故我无法想象 - 特别是因为并非所有需要调用的方法都具有相同数量的参数。或者是否有其他方法可以实现这一目标?

4 个答案:

答案 0 :(得分:2)

在伪代码中

Set Eval (Tree t) {

    switch (t.Operator) {
        case OR:
             Set result = emptySet;
             foreach(child in T.Children) {
                 result = Union(result, Eval(child));
             }
             return result;
        case AND:
             Set result = UniversalSet;
             foreach(child in T.Children) {
                 result = Intersection(result, Eval(child));
             }
             return result;
        case blah: // Whatever.
    }
    // Unreachable.
}

这有帮助吗?

或者您是否希望优化评估顺序,可能会在其上书写...

答案 1 :(得分:2)

我原本希望生成以下树:

enter image description here

(请注意,在您的AST中,OR节点有3个孩子)

无论哪种方式,如果您创建了一个能够创建AST的ANTLR语法(无论是原始图像的形式,还是我上面发布的),这意味着您已经在语法中定义了正确的运算符优先级。在这种情况下,您不应该混淆执行运算符的顺序,因为您的树已经要求首先评估(John <- AND -> Jim)(NOT -> Simon)

您是否可以发布您一直在研究的ANTLR语法?

此外,您正在谈论集合,但在您的示例中,只显示了单个值,因此我得到的印象是您的语言比目前为止显示的要复杂一些。也许你可以解释一下你的实际语言,而不是一个愚蠢的版本?

<强> PS 即可。可以在此处找到创建图像的源:http://graph.gafol.net/elDKbwzbA(ANTLR语法也包含在内)

答案 2 :(得分:1)

我不熟悉ANTLR生成的对象模型,但假设它是这样的:

class BinaryNode : Node
{
    public Node LeftChild;
    public Node RightChild;            
    public readonly string Operator;            
}

class UnaryNode : Node
{
    public Node Child;
    public readonly string Operator;
}

class TerminalNode : Node
{
    public readonly string LeafItem;
}

class Node { }

public class Executor
{
    public IEnumerable<object> Get(string value)
    {
        return null;
    }
    public IEnumerable<object> GetAll()
    {
        return null;
    }

    public IEnumerable<object> GetItems(Node node)
    {
        if (node is TerminalNode)
        {
            var x = node as TerminalNode;
            return Get(x.LeafItem);
        }
        else if (node is BinaryNode)
        {
            var x = node as BinaryNode;
            if (x.Operator == "AND")
            {
                return GetItems(x.LeftChild).Intersect(GetItems(x.RightChild));
            }
            else if (x.Operator == "OR")
            {
                return GetItems(x.LeftChild).Concat(GetItems(x.RightChild));
            }
        }
        else if (node is UnaryNode)
        {
            var x = node as UnaryNode;

            if (x.Operator == "NOT")
            {
                return GetAll().Except(GetItems(x.Child));
            }
        }

        throw new NotSupportedException();
    }
}

但请注意,这会急切地评估查询,这不是最佳的。但它应该让你知道递归是如何工作的。

答案 3 :(得分:0)

我不确定你要做什么,但我想我会把AST变成Func<Person, bool>。每个叶节点可以被评估为Func<Person, bool>例如p => p.Name == "Bill" AND,OR和NOT可以实现为高阶函数,例如:

public static Func<T, bool> And<T>(Func<T, bool> a, Func<T, bool> b)
{
    return t => a(t) && b(T);
}

完成所有这些并将AST折叠为单个Func<Person, bool>后,您可以将其作为参数传递给实现Where()的任何类型的IEnumerable<Person>扩展方法。

换句话说,我首先将AST“编译”为Func<Person, boo>,然后使用LINQ to Objects实际过滤我的集合。编译应该很简单,因为您的AST是Composite设计模式的实现。每个节点都应该能够公开方法Func<Person, bool> Compile()