C # и ANTLR4: обработка директив «включить» при разборе файла

#c# #parsing #include #antlr4 #antlr4cs

#c# #синтаксический анализ #включить #antlr4 #antlr4cs

Вопрос:

Я нахожусь в ситуации, когда, используя ANTLR, я пытаюсь проанализировать входные файлы, которые содержат ссылки на другие файлы внутри них, точно так же, как #include "[insert file name]" на языке C.

Один из предлагаемых подходов:

  1. Проанализируйте корневой файл, сохранив указанные ссылки как узлы (то есть конкретные правила грамматики)
  2. Посетите дерево в поисках «ссылочных» узлов
  3. для каждого ссылочного узла проанализируйте файл, на который ссылается ссылка, и замените узел вновь созданным деревом
  4. повторите этот процесс рекурсивно, чтобы обработать несколько уровней включений

Проблема с этим решением заключается в том, что файлы, на которые ссылаются, могут быть полностью неполными (см. Включает внутри тела функции C). Чтобы анализировать такие файлы, мне пришлось бы реализовать другой анализатор для обработки фрагментированной грамматики.

Существует ли какой-либо допустимый / рекомендуемый подход к (буквально) внедрению нового файла в текущий процесс синтаксического анализа?

Ответ №1:

Одно из решений этой проблемы может быть достигнуто путем переопределения поведения сканера и, в частности, NextToken() метода. Это необходимо, поскольку токен EOF не может быть обработан грамматикой лексера ANTLR (насколько мне известно), и любые действия, связанные с правилом лексера, распознающим EOF, просто игнорируются (как показано в приведенном ниже коде). Таким образом, необходимо реализовать это поведение непосредственно в методе сканирования.

Итак, предположим, что у нас есть грамматика синтаксического анализатора

 parser grammar INCParserGrammar;

@parser::members {
        public static Stack<ICharStream> m_nestedfiles = new Stack<ICharStream>();
}

options { tokenVocab = INCLexerGrammar; }

/*
 * Parser Rules
 */

compileUnit
    :   (include_directives | ANY )  ENDOFFILE
    ;

include_directives : INCLUDEPREFIX FILE DQUOTE
                     ;
 

A static public Stack<ICharStream> (т.Е. mySpecialFileStack ) должен быть введен внутри членов грамматики. Этот стек будет использоваться для хранения пар символов, связанных с файлами, которые принимают участие в синтаксическом анализе. Потоки символов передаются в
этот стек по мере обнаружения новых файлов с помощью операторов include

и грамматика лексера

    lexer grammar INCLexerGrammar;

   @lexer::header {
    using System;
    using System.IO;
   }

   @lexer::members { 
    string file;
    ICharStream current;
    
   }


/*
 * Lexer Rules
 */
INCLUDEPREFIX : '#include'[ t] '"' {                                                 
                                      Mode(INCLexerGrammar.FILEMODE);
                                    };

// The following ruls has always less length matched string that the the rule above
ANY : ~[#]  ;


ENDOFFILE : EOF { // Any actions in the this rule are ignored by the ANTLR lexer };


////////////////////////////////////////////////////////////////////////////////////////////////////////

mode FILEMODE;
FILE : [a-zA-Z][a-zA-Z0-9_]*'.'[a-zA-Z0-9_]  {  file= Text;
                                                StreamReader s = new StreamReader(file);
                                                INCParserGrammar.m_nestedfiles.Push(_input);                                                
                                                current =new AntlrInputStream(s);           
                                            
                                             };
DQUOTE: '"'  {  
                this._input = current;
                Mode(INCLexerGrammar.DefaultMode);  };
 

Переопределенное тело метода nextToken() будет помещено в файл .g4.cs, целью которого является расширение
сгенерированного класса сканера, учитывая, что сгенерированный класс сканера украшен ключевым словом «partial»

После создания частичного класса сканера, связанного с данной грамматикой, перейдите к исходному коду класса лексера ANTLR4, как указано в среде выполнения ANTLR, и скопируйте ВЕСЬ исходный код в этот новый метод, а в среднем блоке do-while (сразу после блока try-catch) добавьте следующий код:

 if (this._input.La(1) == -1)
{
    if ( mySpecialFileStack.Count == 0 )
        this._hitEOF = true;
    else
        this._input = mySpecialFileStack.Pop();
}
 

Полное тело переопределения метода nextToken()

 public override IToken NextToken() {
            int marker = this._input != null ? this._input.Mark() : throw new InvalidOperationException("nextToken requires a non-null input stream.");
            label_3:
            try {
                while (!this._hitEOF) {
                    this._token = (IToken)null;
                    this._channel = 0;
                    this._tokenStartCharIndex = this._input.Index;
                    this._tokenStartCharPositionInLine = this.Interpreter.Column;
                    this._tokenStartLine = this.Interpreter.Line;
                    this._text = (string)null;
                    do {
                        this._type = 0;
                        int num;
                        try {
                            num = this.Interpreter.Match(this._input, this._mode);
                        } catch (LexerNoViableAltException ex) {
                            this.NotifyListeners(ex);
                            this.Recover(ex);
                            num = -3;
                        }

                        if (this._input.La(1) == -1) {
                            if (INCParserGrammar.m_nestedfiles.Count == 0 ) {
                                this._hitEOF = true;
                            }
                            else
                            {
                                this._input = INCParserGrammar.m_nestedfiles.Pop();
                            }
                        }

                        if (this._type == 0)
                            this._type = num;
                        if (this._type == -3)
                            goto label_3;
                    }
                    while (this._type == -2);
                    if (this._token == null)
                        this.Emit();
                    return this._token;
                }
                this.EmitEOF();
                return this._token;
            } finally {
                this._input.Release(marker);
            }
        }

 

Теперь, когда вы распознаете файл внутри своего кода, который должен быть проанализирован, просто добавьте следующее действие

 FILE
    : [a-zA-Z][a-zA-Z0-9_]*'.'[a-zA-Z0-9_]  {
        StreamReader s = new StreamReader(Text);
        mySpecialFileStack.Push(_input);                                                
        _input = new AntlrInputStream(s);                                               
    };
    
DQUOTE: '"'  {  this._input = current;
            Mode(INCLexerGrammar.DefaultMode);  };
//***Warning:***
// Be careful when your file inclusion is enclosed inside quotes or other symbols, or if  
// the filename-to-be-included is not the last token that defines an inclusion: `_input`  
// should only be switched AFTER the inclusion detection is completely found (i.e. after  
// the closing quote has been recognized).  

 

Наконец, основная программа приведена ниже, где очевидно, что корневой файл добавляется первым в стек ICharStream

  static void Main(string[] args) {
            var a = new StreamReader("./root.txt");
            var antlrInput = new AntlrInputStream(a);
            INCParserGrammar.m_nestedfiles.Push(antlrInput);
            var lexer = new INCLexerGrammar(antlrInput);
            var tokens = new BufferedTokenStream(lexer);
            var parser = new INCParserGrammar(tokens);
            parser.compileUnit();
            
        }
 

Ответ №2:

Чтение ответа г-на Григориса помогло мне найти другое возможное решение моей проблемы:

Пытаясь выяснить, как работает предлагаемое решение, я наткнулся на public virtual IToken EmitEOF() метод. Если код, предоставленный г-ном Григорисом, помещается внутри этой функции (с незначительными изменениями), кажется, что все работает так, как задумано.

Это дало мне возможность переопределить функциональность EmitEOF() непосредственно из @members блока lexer, без необходимости создавать совершенно новый файл или понимать, как работает мой текущий NextToken() метод синтаксического анализа.

Грамматика лексера:

 lexer grammar INCLexerGrammar;  
  
@lexer::header {  
    using System;  
    using System.IO;  
    using System.Collections.Generic;  
}  
  
@lexer::members {   
  
    private Stack<ICharStream> _nestedFiles = new Stack<ICharStream>();  
      
    public override IToken EmitEOF(){  
        if (_nestedFiles.Count == 0 ) {  
            return base.EmitEOF();  
        };  
        this._hitEOF = false;  
        this._input = _nestedFiles.Pop();  
        return this.NextToken();  
    }  
}  
  
/////////////////////////////////////////////////////////////////////////////////////  
// Default Mode /////////////////////////////////////////////////////////////////////  
/////////////////////////////////////////////////////////////////////////////////////  
  
// Skipped because we don't want to hide INCLUDEPREFIX's existance from parser  
INCLUDEPREFIX : '#include'[ t] '"' { Mode(INCLexerGrammar.FILEMODE); } -> skip;  

// This is the only valid token our Grammar accepts
ANY : ~[#]  ;  
  
/////////////////////////////////////////////////////////////////////////////////////  
mode FILEMODE; //////////////////////////////////////////////////////////////////////  
/////////////////////////////////////////////////////////////////////////////////////  
  
// Skipped because we don't want to hide FILE's existance from parser  
FILE : [a-zA-Z][a-zA-Z0-9_]*'.'[a-zA-Z0-9_]  {   
  
    // Create new StreamReader from the file mentioned  
    StreamReader s = new StreamReader(Text);  
      
    // Push the old stream to stack  
    _nestedFiles.Push(_input);  
      
    // This new stream will be popped and used right after, on DQUOTE.  
    _nestedFiles.Push(new AntlrInputStream(s));  

} -> skip;  
  
// Skipped because we don't want to hide DQUOTE's existance from parser  
DQUOTE: '"' { 

    // Injecting the newly generated Stream.  
    this._input = _nestedFiles.Pop();

    Mode(INCLexerGrammar.DefaultMode);

} -> skip;
 

Грамматика синтаксического анализатора:

 parser grammar INCParserGrammar;  
  
options { tokenVocab = INCLexerGrammar; }  
  
// Our Grammar contains only ANY tokens. Include directives  
// and other Tokens exists only for helping lexer to  
// inject the contents of other files inside the current  
// scanning process.  
  
compileUnit  
  :  ANY  EOF  
  ;
 

Вызовы выполнения:

 // [...]

var myRootFile = new StreamReader("./root.txt");
var myAntlrInputStream = new AntlrInputStream(myRootFile);

var lexer = new INCLexerGrammar(myAntlrInputStream);
var tokens = new BufferedTokenStream(lexer);
var parser = new INCParserGrammar(tokens);

parser.compileUnit();

// [...]