C# 和 ANTLR4:解析文件时处理 "include" 指令
C# and ANTLR4: Handling "include" directives when parsing a file
我的情况是,使用 ANTLR,我正在尝试解析包含对其中其他文件的引用的输入文件,就像 C 语言的 #include "[insert file name]"
一样。
一种建议的方法是:
- 解析根文件,将所述引用保存为节点(因此,特定语法规则)
- 访问树搜索“参考”节点
- 对于每个引用节点,解析引用的文件并将节点替换为新生成的树
- 递归地重复此过程,以处理多级包含
此解决方案的问题是引用的文件可能完全是部分文件(请参阅包含在 C 函数体内)。为了解析这些文件,我将不得不实现一个不同的解析器来处理碎片化的语法。
是否有任何 valid/suggested 方法(从字面上)将新文件注入正在进行的解析过程?
这个问题的一个解决方案可以通过覆盖扫描器的行为,特别是 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
)应该被引入语法的成员中。该堆栈将用于存储与参与解析的文件关联的角色流。字符流被推送到
这个堆栈作为包含语句遇到新文件
和词法分析器语法
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 文件中,目的是扩展
生成的扫描器 class 鉴于生成的扫描器 class 用“部分”关键字修饰
在生成与给定语法关联的部分扫描器 Class 后,导航至该语法的源代码
ANTLR4 Lexer Class 在 ANTLR Runtime 中给出,并将所有原始代码复制到这个新方法中,并且,
在中间的 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();
}
阅读 Grigoris 先生的回答帮助我发现了另一种可能的解决方案:
在尝试弄清楚建议的解决方案如何工作时,我偶然发现了 public virtual IToken EmitEOF()
方法。如果将 Grigoris 先生提供的代码放入此函数中(稍作更改),一切似乎都按预期工作。
这让我有机会直接从词法分析器的 @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();
// [...]
我的情况是,使用 ANTLR,我正在尝试解析包含对其中其他文件的引用的输入文件,就像 C 语言的 #include "[insert file name]"
一样。
一种建议的方法是:
- 解析根文件,将所述引用保存为节点(因此,特定语法规则)
- 访问树搜索“参考”节点
- 对于每个引用节点,解析引用的文件并将节点替换为新生成的树
- 递归地重复此过程,以处理多级包含
此解决方案的问题是引用的文件可能完全是部分文件(请参阅包含在 C 函数体内)。为了解析这些文件,我将不得不实现一个不同的解析器来处理碎片化的语法。
是否有任何 valid/suggested 方法(从字面上)将新文件注入正在进行的解析过程?
这个问题的一个解决方案可以通过覆盖扫描器的行为,特别是 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
)应该被引入语法的成员中。该堆栈将用于存储与参与解析的文件关联的角色流。字符流被推送到
这个堆栈作为包含语句遇到新文件
和词法分析器语法
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 文件中,目的是扩展 生成的扫描器 class 鉴于生成的扫描器 class 用“部分”关键字修饰
在生成与给定语法关联的部分扫描器 Class 后,导航至该语法的源代码 ANTLR4 Lexer Class 在 ANTLR Runtime 中给出,并将所有原始代码复制到这个新方法中,并且, 在中间的 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();
}
阅读 Grigoris 先生的回答帮助我发现了另一种可能的解决方案:
在尝试弄清楚建议的解决方案如何工作时,我偶然发现了 public virtual IToken EmitEOF()
方法。如果将 Grigoris 先生提供的代码放入此函数中(稍作更改),一切似乎都按预期工作。
这让我有机会直接从词法分析器的 @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();
// [...]