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

时间:2017-07-20 17:32:02

标签: antlr4

我有一个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的目标之一是保持语法文件可重用且与操作无关,我试图从该文件中删除操作并将其移动到树步行器。我用以下代码对它进行了第一次尝试:

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方法的返回类型应该是无效的,因此它们没有办法在上游返回聚合值。其次,我没有看到如何访问&#34; i1&#34;和&#34; i2&#34;从沃克代码,所以我无法区分&#34;标识符&#34;的两个实例。在那条规则中。

有关如何为此目的将操作与语法分开的任何建议吗?

我应该在这里使用访问者而不是听众,因为访问者有能力返回值吗?如果我使用访问者,我该如何解决区分&#34; i1&#34;和&#34; i2&#34; (如上所述)?

访问者是否仅在规则的退出处执行其操作(与侦听器不同,对于入口和出口都存在)?例如,如果我必须在规则&#34; top&#34;的条目下初始化列表,我该如何使用仅在规则结束时执行的访问者?我是否需要为此目的使用enterTop监听器?

编辑:在初始发布后,我修改了规则&#34; top&#34;创建并返回一个列表,并将该列表传递回主程序进行打印。这是为了说明为什么我需要一个代码的初始化机制。

2 个答案:

答案 0 :(得分:1)

根据您的尝试,我认为您可以从使用ANTLR的BaseVisitor类而不是BaseListener类中受益。

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

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是一个泛型类,其中generic参数确定访问方法的返回类型。对于您的示例,我设置了通用参数Object,但您甚至可以创建自己的类,其中包含您要保留的信息并将其用于通用参数。

以下是一些更有用的methods BaseVisitor继承,可能会帮助您解决问题。

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

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

作为旁注,ANTLR4中的ANTLRFileStream类为deprecated。 建议改为使用CharStreams

答案 1 :(得分:0)

正如Terence Parr在“权威参考”中所指出的,访问者和监听者之间的一个主要区别是访问者可以返回值。这很方便。但是Listener也有一席之地! What I do for listener is exemplified in this answer。当然,有一些更简单的方法可以解析数字列表,但我做了一个答案,展示了一个完整的工作示例,说明如何将监听器的返回值聚合到以后可以使用的公共数据结构中

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

仔细观察Listener类,我包含一个公共数据集合 - 在这种情况下为List<double> - 用于收集在侦听器事件中解析或计算的值。您可以使用您喜欢的任何数据结构:另一个自定义类,一个列表,一个队列,一个堆栈(伟大的用于计算和表达式评估),无论您喜欢什么。

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