从 antlr4 语法中消除嵌入式操作

eliminating embedded actions from antlr4 grammar

我有一个 antlr 语法,其中嵌入的操作用于自下而上收集数据并构建聚合数据结构。下面给出了一个简短的版本,其中只打印了聚合数据结构(即在这个简短的示例代码中没有为它们创建 类)。

grammar Sample;
top returns [ArrayList l]
    @init { $l = new ArrayList<String>(); }
    : (mid { $l.add($mid.s); } )* ;
mid returns [String s]
    : i1=identifier 'hello' i2=identifier
        { $s = $i1.s + " bye " + $i2.s; }
    ;
identifier returns [String s]
    : ID { $s = $ID.getText(); } ;
ID  : [a-z]+ ;
WS  : [ \t\r\n]+ -> skip ;

其对应的主程序为:

public class Main {
  public static void main( String[] args) throws Exception 
  {
    SampleLexer lexer = new SampleLexer( new ANTLRFileStream(args[0]));
    CommonTokenStream tokens = new CommonTokenStream( lexer );
    SampleParser parser = new SampleParser( tokens );
    ArrayList<String> top = parser.top().l;
    System.out.println(top);
  }
}

样本测试是:

aaa hello bbb
xyz hello pqr

由于 antlr 的目标之一是保持语法文件的可重用性和动作独立性,因此我试图从该文件中删除动作并将其移至 tree walker。我用下面的代码尝试了一下:

public class Main {
  public static void main( String[] args) throws Exception 
  {
    SampleLexer lexer = new SampleLexer( new ANTLRFileStream(args[0]));
    CommonTokenStream tokens = new CommonTokenStream( lexer );
    SampleParser parser = new SampleParser( tokens );
    ParseTree tree = parser.top();
    ParseTreeWalker walker = new ParseTreeWalker();
    walker.walk( new Walker(), tree );
  }
}
public class Walker extends SampleBaseListener {
  public void exitTop(SampleParser.TopContext ctx ) {
    System.out.println( "Exit Top : " + ctx.mid() );
  }
  public String exitMid(SampleParser.MidContext ctx ) {
    return ctx.identifier() + " bye "; // ignoring the 2nd instance here
  }
  public String exitIdentifier(SampleParser.IdentifierContext ctx ) {
    return ctx.ID().getText() ;
  }
}

但显然这是错误的,因为至少,Walker 方法的 return 类型应该是 void,所以它们没有办法 return 向上游聚合值。其次,我看不到如何从 walker 代码访问 "i1" 和 "i2" 的方法,因此我无法区分该规则中 "identifier" 的两个实例。

对于如何为此目的将动作与语法分开有什么建议吗?

我应该在这里使用访问者而不是侦听器,因为访问者具有 returning 值的能力吗?如果我使用访问者,如何解决 "i1" 和 "i2" 之间的区分问题(如上所述)?

访问者是否仅在规则出口处执行其操作(不像监听器,入口和出口都存在)?例如,如果我必须在规则 "top" 的入口处初始化列表,我该如何处理仅在规则结束时执行的访问者?为此,我需要一个 enterTop 侦听器吗?

编辑: 在最初的 post 之后,我修改了规则 "top" 来创建和 return 一个列表,并传递这个列表返回到主程序进行打印。这是为了说明为什么我需要代码的初始化机制。

根据您的尝试,我认为您可能会受益于使用 ANTLR 的 BaseVisitor Class 而不是 BaseListener Class.

假设你的语法是这样的(我概括了它,我将在下面解释变化):

grammar Sample;

top : mid* ;
mid : i1=identifier 'hello' i2=identifier ;
identifier : ID ;

ID  : [a-z]+ ;
WS  : [ \t\r\n]+ -> skip ;

那么您的助行器将如下所示:

public class Walker extends SampleBaseVisitor<Object> {
    public ArrayList<String> visitTop(SampleParser.TopContext ctx) {
        ArrayList<String> arrayList = new ArrayList<>();

        for (SampleParser.MidContext midCtx : ctx.mid()) {
            arrayList.add(visitMid(midCtx));
        }

        return arrayList;
    }

    public String visitMid(SampleParser.MidContext ctx) {
        return visitIdentifier(ctx.i1) + " bye " + visitIdentifier(ctx.i2);
    }

    public String visitIdentifier(SampleParser.IdentifierContext ctx) {
        return ctx.getText();
    }
}

这允许您访问并获得您想要的任何规则的结果。

您可以访问 i1i2,因为您通过访问者方法标记了它们。请注意,您并不真的需要 identifier 规则,因为它只包含一个标记,您可以直接在 visitMid 中访问标记的文本,但这实际上是个人偏好。

您还应注意,SampleBaseVisitor 是泛型 class,其中泛型参数决定了访问方法的 return 类型。对于您的示例,我设置了通用参数 Object,但您甚至可以创建自己的 class,其中包含您想要保留的信息并将其用于您的通用参数。

这里有一些更有用的 methodsBaseVisitor 继承了它们,可能会对您有所帮助。

最后,您的主要方法最终看起来像这样:

public static void main( String[] args) throws IOException {
    FileInputStream fileInputStream = new FileInputStream(args[0]);
    SampleLexer lexer = new SampleLexer(CharStreams.fromStream(fileInputStream));
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    SampleParser parser = new SampleParser(tokens);

    for (String string : new Walker().visitTop(parser.top())) {
        System.out.println(string);
    }
}

作为旁注,ANTLRFileStream class 在 ANTLR4 中是 deprecated。 建议使用 CharStreams 代替。

正如 Terence Parr 在 Definitive Reference 中指出的那样,访问者和听众之间的一个主要区别是访问者可以 return 值。这可能很方便。但是 Listener 也有一席之地! 。诚然,有更简单的方法来解析数字列表,但我做出这个回答是为了展示一个 完整且有效的示例,说明如何将来自侦听器的 return 值聚合到 public以后可以消费的数据结构.

public class ValuesListener : ValuesBaseListener
{
    public List<double> doubles = new List<double>(); // <<=== SEE HERE
    public override void ExitNumber(ValuesParser.NumberContext context)
    {
        doubles.Add(Convert.ToDouble(context.GetChild(0).GetText()));
    }
}

仔细观察监听器 class,我包含了一个 public 数据集合——在本例中是一个 List<double>——以收集在监听器事件中解析或计算的值。您可以使用您喜欢的任何数据结构:另一个自定义 class、列表、队列、堆栈(非常好 用于计算和表达式评估),任何您喜欢的。

因此,虽然访问者可以说更灵活,但侦听器也是一个强有力的竞争者,具体取决于您希望如何汇总结果。