C# 和 ANTLR4:分析文件时处理"include"指令



我的情况是,使用ANTLR,我试图解析输入文件,其中包含对其他文件的引用,就像C语言的#include "[insert file name]"

一个建议的方法是:

  1. 解析根文件,将所述引用保存为节点(因此,特定语法规则)
  2. 访问树查找"参考";节点
  3. 对于每个参考节点,解析所引用的文件并用新生成的树
  4. 替换节点
  5. 递归地重复此过程,以处理多个级别的内含物

这个解决方案的问题是引用的文件可能完全是局部的(参见C函数体内的include)。为了解析这样的文件,我必须实现一个不同的解析器来处理碎片化的语法。

是否有任何有效/建议的方法(字面上)将新文件注入正在进行的解析过程中?

这个问题的一个解决方案可以通过重写Scanner的行为,特别是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
;

应在语法成员内部引入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&;关键字

生成与给定语法关联的部分Scanner Class之后,导航到ANTLR运行时中给出的ANTLR4 Lexer类,并将所有原始代码复制到这个新方法中,在中间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();

}

阅读Mr. Grigoris的回答帮助我发现了我的问题的另一个可能的解决方案:

在试图弄清楚建议的解决方案如何工作时,我偶然发现了public virtual IToken EmitEOF()方法。如果Mr. Grigoris提供的代码被放置在这个函数中(只做了一些小改动),那么一切似乎都按预期工作。

这让我有机会直接从lexer的@members覆盖EmitEOF()功能,而不必创建一个全新的文件或理解我当前解析器的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();
// [...]

最新更新