从 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();
}
}
这允许您访问并获得您想要的任何规则的结果。
您可以访问 i1
和 i2
,因为您通过访问者方法标记了它们。请注意,您并不真的需要 identifier
规则,因为它只包含一个标记,您可以直接在 visitMid
中访问标记的文本,但这实际上是个人偏好。
您还应注意,SampleBaseVisitor
是泛型 class,其中泛型参数决定了访问方法的 return 类型。对于您的示例,我设置了通用参数 Object
,但您甚至可以创建自己的 class,其中包含您想要保留的信息并将其用于您的通用参数。
这里有一些更有用的 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);
}
}
作为旁注,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、列表、队列、堆栈(非常好 用于计算和表达式评估),任何您喜欢的。
因此,虽然访问者可以说更灵活,但侦听器也是一个强有力的竞争者,具体取决于您希望如何汇总结果。
我有一个 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();
}
}
这允许您访问并获得您想要的任何规则的结果。
您可以访问 i1
和 i2
,因为您通过访问者方法标记了它们。请注意,您并不真的需要 identifier
规则,因为它只包含一个标记,您可以直接在 visitMid
中访问标记的文本,但这实际上是个人偏好。
您还应注意,SampleBaseVisitor
是泛型 class,其中泛型参数决定了访问方法的 return 类型。对于您的示例,我设置了通用参数 Object
,但您甚至可以创建自己的 class,其中包含您想要保留的信息并将其用于您的通用参数。
这里有一些更有用的 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);
}
}
作为旁注,ANTLRFileStream
class 在 ANTLR4 中是 deprecated。
建议使用 CharStreams
代替。
正如 Terence Parr 在 Definitive Reference 中指出的那样,访问者和听众之间的一个主要区别是访问者可以 return 值。这可能很方便。但是 Listener 也有一席之地!
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、列表、队列、堆栈(非常好 用于计算和表达式评估),任何您喜欢的。
因此,虽然访问者可以说更灵活,但侦听器也是一个强有力的竞争者,具体取决于您希望如何汇总结果。