Семантическое устранение неоднозначности неоднозначного синтаксиса

#antlr4

#antlr4

Вопрос:

Используя Antlr 4, у меня возникла ситуация, которую я не уверен, как разрешить. Первоначально я задал вопрос в https://groups.google.com/forum /#!тема/antlr-обсуждение/1yxxxAvU678 на дискуссионном форуме Antlr. Но этот форум, похоже, не получает большого трафика, поэтому я снова спрашиваю здесь.

У меня есть следующая грамматика:

 expression
    : ...
    | path
    ;

path
    : ...
    | dotIdentifierSequence
    ;

dotIdentifierSequence 
    : identifier (DOT identifier)*
    ;
  

Проблема здесь в том, что семантически это dotIdentifierSequence может означать несколько вещей, и не все из них являются «путями». Но на данный момент все они распознаются как пути в дереве синтаксического анализа, а затем мне нужно обработать их специально в моем посетителе.

Но что мне действительно хотелось бы, так это способ выражения способов использования dotIdentifierSequence, которые не являются путями в expression правиле, а не в path правиле, и все еще имеют dotIdentifierSequence в path для обработки способов использования пути.

Чтобы было ясно, последовательность dotIdentifierSequence может быть любой из следующих:

  1. Путь — это грамматика, подобная SQL, и выражение пути будет похоже на ссылку на таблицу или столбец в SQL, например a.b.c
  2. Имя класса Java — например com.acme.SomeJavaType
  3. Ссылка на статическое поле Java — например com.acme.SomeJavaType.SOME_FIELD
  4. Ссылка на значение перечисления Java — например com.acme.Gender.MALE

Идея заключается в том, что во время посещения «dotIdentifierSequence как путь» разрешается как тип, сильно отличающийся от других способов использования.

Есть идеи, как я могу это сделать?

Ответ №1:

Проблема здесь в том, что вы пытаетесь провести различие между «путями» при создании в анализаторе. Построение путей внутри лексера было бы проще (следует псевдокод):

 grammar T;

tokens {
  JAVA_TYPE_PATH,
  JAVA_FIELD_PATH
}

// parser rules

PATH
 : IDENTIFIER ('.' IDENTIFIER)*
   {
     String s = getText();
     if (s is a Java class) {
       setType(JAVA_TYPE_PATH);
     } else if (s is a Java field) {
       setType(JAVA_FIELD_PATH);
     }
   }
 ;

fragment IDENTIFIER : [a-zA-Z_] [a-zA-Z_0-9]*;
  

и затем в синтаксическом анализаторе вы бы сделали:

 expression
 : JAVA_TYPE_PATH   #javaTypeExpression
 | JAVA_FIELD_PATH  #javaFieldExpression  
 | PATH             #pathExpression
 ;
  

Но тогда, конечно, ввод, подобный этому java./*comment*/lang.String , будет ошибочно помечен.

Обработка всего этого в анализаторе означала бы вручную просматривать поток токенов и проверять, существует ли тип Java или поле.

Быстрая демонстрация:

 grammar T;

@parser::members {

  String getPathAhead() {

    Token token = _input.LT(1);

    if (token.getType() != IDENTIFIER) {
      return null;
    }

    StringBuilder builder = new StringBuilder(token.getText());

    // Try to collect ('.' IDENTIFIER)*
    for (int stepsAhead = 2; ; stepsAhead  = 2) {

      Token expectedDot = _input.LT(stepsAhead);
      Token expectedIdentifier = _input.LT(stepsAhead   1);

      if (expectedDot.getType() != DOT || expectedIdentifier.getType() != IDENTIFIER) {
        break;
      }

      builder.append('.').append(expectedIdentifier.getText());
    }

    return builder.toString();
  }

  boolean javaTypeAhead() {

    String path = getPathAhead();

    if (path == null) {
      return false;
    }

    try {
      return Class.forName(path) != null;
    } catch (Exception e) {
      return false;
    }
  }

  boolean javaFieldAhead() {

    String path = getPathAhead();

    if (path == null || !path.contains(".")) {
      return false;
    }

    int lastDot = path.lastIndexOf('.');
    String typeName = path.substring(0, lastDot);
    String fieldName = path.substring(lastDot   1);

    try {
      Class<?> clazz = Class.forName(typeName);
      return clazz.getField(fieldName) != null;
    } catch (Exception e) {
      return false;
    }
  }
}

expression
 : {javaTypeAhead()}?  path    #javaTypeExpression
 | {javaFieldAhead()}? path    #javaFieldExpression
 | path                        #pathExpression
 ;

path
 : dotIdentifierSequence
 ;

dotIdentifierSequence
 : IDENTIFIER (DOT IDENTIFIER)*
 ;

IDENTIFIER
 : [a-zA-Z_] [a-zA-Z_0-9]*
 ;

DOT
 : '.'
 ;
  

который можно протестировать со следующим классом:

 package tl.antlr4;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.misc.NotNull;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

public class Main {

    public static void main(String[] args) {

        String[] tests = {
            "mu",
            "tl.antlr4.The",
            "java.lang.String",
            "foo.bar.Baz",
            "tl.antlr4.The.answer",
            "tl.antlr4.The.ANSWER"
        };

        for (String test : tests) {
            TLexer lexer = new TLexer(new ANTLRInputStream(test));
            TParser parser = new TParser(new CommonTokenStream(lexer));
            ParseTreeWalker.DEFAULT.walk(new TestListener(), parser.expression());
        }
    }
}

class TestListener extends TBaseListener {

    @Override
    public void enterJavaTypeExpression(@NotNull TParser.JavaTypeExpressionContext ctx) {
        System.out.println("JavaTypeExpression  -> "   ctx.getText());
    }

    @Override
    public void enterJavaFieldExpression(@NotNull TParser.JavaFieldExpressionContext ctx) {
        System.out.println("JavaFieldExpression -> "   ctx.getText());
    }

    @Override
    public void enterPathExpression(@NotNull TParser.PathExpressionContext ctx) {
        System.out.println("PathExpression      -> "   ctx.getText());
    }
}

class The {
    public static final int ANSWER = 42;
}
  

который выведет на консоль следующее:

 PathExpression      -> mu
JavaTypeExpression  -> tl.antlr4.The
JavaTypeExpression  -> java.lang.String
PathExpression      -> foo.bar.Baz
PathExpression      -> tl.antlr4.The.answer
JavaFieldExpression -> tl.antlr4.The.ANSWER
  

Комментарии:

1. Ах, я никогда не рассматривал возможность переноса этого в лексер. Хотя это имеет смысл, поскольку на самом деле все они представляют разные типы токенов. Спасибо за оригинальное решение!

2. Из вопроса я предположил, что OP уже выполняет такую проверку, чтобы выяснить, является ли такой идентификатор путем или ссылкой на класс / тип, и хотел другого решения. Кстати. с функциональной точки зрения не имеет значения, есть ли у вас эта проверка в синтаксическом анализаторе или в лексере, но наличие ее в лексере означает: ручная обработка пробелов.