使用标记规则编写类型安全的访问者

writing a typesafe visitor with labeled rules

我正在将我的原型从侦听器模式迁移到访问者模式。在原型中,我有这样一个语法片段:

thingList: thing+ ;

thing
  : A aSpec    # aRule
  | B bSpec    # bRule
  ;

转向访客模式,我不确定我是怎么写的visitThingList。每个访问者 return 都是“节点”的专门子class,我会喜欢什么时候能够写这样的东西,比如说“thingList”关心列表中的第一件事...

visitThingList(cx: ThingListContext): ast.ThingList {
  ...
  const firstThing = super.visit(cx.thing(0));

问题出在打字上。每次访问 return 都是一种特殊类型,它是 ast.Node 的子 class。因为我使用的是 super.visit,return 值将是基础 class 我的节点树。但是,我知道因为我正在看语法 因为我写了 vistARulevisitBRule 访问的结果将是类型 ast.Thing.

所以我们让 visitThingList 表达它对 cast 的期望...

visitThingList(cx: ThingListContext): ast.ThingList {
  const firstThing = super.visit(cx.thing(0));
  if (!firstThing instanceof ast.Thing) {
    throw "no matching visitor for thing";
  }
  // firstThing is now known to be of type ast.Thing
  ...

在我的大部分翻译器中,ast 节点的类型问题是一个编译时问题,我在我的编辑器中修复它们。在这种情况下,我正在生成一个更脆弱的步行,它只会在运行时揭示脆弱性,然后仅在特定输入下显示。

我想我可以改变我的语法,使编码成为可能 通过创建 vistThing() 入口点

输入对 vistThingList() 的期望
thingList: thing+ ;
thing: aRule | bRule;
aRule: A aSpec;
bRule: B bSpec;

输入 vistThing() 以符合预期:

visitThing(cx: ThingContext): ast.Thing { }
visitThingList(cx: ThingListContext) {
  const firstThing: ast.Thing = this.visitThing(cx.thing(0));

现在visitThingList可以调用this.visitThing()并且确保thing匹配return的所有规则的类型强制ast.Thing属于visitThing().如果我确实为事物创建了一个新规则,编译器将强制我更改 visitThing() 的 return 类型,如果我将它设为 return 不是事物的东西,visitThingList() 将显示类型错误。

虽然这似乎也是错误的,因为我觉得我不应该为了访问它而必须更改我的语法。

我是 ANTLR 的新手,想知道是否有更好的模式或方法。

When I was using the listener pattern, I wrote something like:

enterThing(cx: ThingContext) { }
enterARule(cx : ARuleContext) { }
enterBRule(cx : BRuleContext) { }

不完全是:对于像 thing 这样的标记规则,侦听器将不包含 enterThing(...)exitThing(...) 方法。只会创建标签 aSpecbSpecenter...exit... 方法。

How would I write the visitor walk without changing the grammar?

我不明白你为什么需要更改语法。当你保持你提到的语法时:

thingList: thing+ ;

thing
  : A aSpec    # aRule
  | B bSpec    # bRule
  ;

然后可以使用以下访问者(同样,没有 visitThing(...) 方法!):

public class TestVisitor extends TBaseVisitor<Object> {

    @Override
    public Object visitThingList(TParser.ThingListContext ctx) {
        ...
    }

    @Override
    public Object visitARule(TParser.ARuleContext ctx) {
        ...
    }

    @Override
    public Object visitBRule(TParser.BRuleContext ctx) {
        ...
    }

    @Override
    public Object visitASpec(TParser.ASpecContext ctx) {
        ...
    }

    @Override
    public Object visitBSpec(TParser.BSpecContext ctx) {
        ...
    }
}

编辑

I do not know how, as i iterate over that, to call the correct visitor for each element

你不需要知道。您可以简单地调用访问者的 (super) visit(...) 方法,将调用正确的方法:

class TestVisitor extends TBaseVisitor<Object> {

    @Override
    public Object visitThingList(TParser.ThingListContext ctx) {
        for (TParser.ThingContext child : ctx.thing()) {
            super.visit(child);
        }
        return null;
    }

    ...
}

而且您甚至不需要实现所有方法。你没有实现的那些,将有一个默认的visitChildren(ctx),导致(顾名思义)它们下面的所有子节点都被遍历。

在您的情况下,以下访问者已经导致 visitASpecvisitBSpec 被调用:

class TestVisitor extends TBaseVisitor<Object> {

    @Override
    public Object visitASpec(TParser.ASpecContext ctx) {
        System.out.println("visitASpec");
        return null;
    }

    @Override
    public Object visitBSpec(TParser.BSpecContext ctx) {
        System.out.println("visitBSpec");
        return null;
    }
}

您可以这样测试(在 Java 中):

String source = "... your input here ...";
TLexer lexer = new TLexer(CharStreams.fromString(source));
TParser parser = new TParser(new CommonTokenStream(lexer));
TestVisitor visitor = new TestVisitor();
visitor.visit(parser.thingList());