手编码解析器

对于你所有的编译器大师,我想编写一个递归下降解析器,我想只用代码来做。 没有从其他语法生成词法分析器和解析器并且不告诉我阅读龙书,我最终会到达那里。

我想进入关于为合理的简单语言实现词法分析器和解析器的细节,比如说CSS。 我想做对了。

这可能最终会成为一系列问题,但现在我开始使用词法分析器了。 可以在此处找到CSS的标记规则。

我发现自己编写这样的代码(希望你可以从这个代码片段中推断出其余部分):

public CssToken ReadNext() { int val; while ((val = _reader.Read()) != -1) { var c = (char)val; switch (_stack.Top) { case ParserState.Init: if (c == ' ') { continue; // ignore } else if (c == '.') { _stack.Transition(ParserState.SubIdent, ParserState.Init); } break; case ParserState.SubIdent: if (c == '-') { _token.Append(c); } _stack.Transition(ParserState.SubNMBegin); break; 

这个叫什么? 我离合理的东西有多远了? 我试图平衡一些在效率和易于使用方面公平的东西,使用堆栈来实现某种状态机工作得很好,但我不确定如何继续这样做。

我所拥有的是一个输入流,我可以从中一次读取1个字符。 我现在不做任何看法,我只是阅读角色,然后根据当前状态尝试做一些事情。

我真的很想进入编写可重用代码片段的思维模式。 这种Transition方法目前是这样做的,它将弹出堆栈的当前状态,然后以相反的顺序推送参数。 这样,当我编写Transition(ParserState.SubIdent, ParserState.Init) ,它将“调用”一个子例程SubIdent ,它将在完成后返回Init状态。

解析器将以大致相同的方式实现,目前,像这样的单个大方法中的所有内容都允许我在找到令牌时轻松返回令牌,但它也迫使我将所有内容保存在一个单一的大方法中。 有没有一种很好的方法将这些标记化规则拆分为单独的方法?

你正在写的是一个下推自动机。 这通常比编写词法分析器所需的function更强大,如果你正在为像CSS这样的现代语言编写词法分析器,那肯定会过度。 递归下降解析器接近下推自动机,但递归下降解析器更容易编写和理解。 大多数解析器生成器生成下推自动机。

Lexers几乎总是被写成有限状态机,就像你的代码一样,除了摆脱“堆栈”对象。 有限状态机与正则表达式密切相关(实际上,它们可以certificate彼此相当)。 在设计这样的解析器时,通常从正则表达式开始并使用它们来创建确定性有限自动机,在转换中使用一些额外的代码来记录每个令牌的开头和结尾。

有工具可以做到这一点。 lex工具及其后代是众所周知的,并已被翻译成多种语言。 ANTLR工具链还有一个词法分析器组件。 我首选的工具是支持它的平台上的ragel 。 手动编写词法分析器大部分时间都没什么好处,这些工具生成的代码可能更快,更可靠。

如果你想手工编写自己的词法分析器,那么好的词法分析器通常看起来像这样:

 function readToken() // note: returns only one token each time while !eof c = peekChar() if c in A-Za-z return readIdentifier() else if c in 0-9 return readInteger() else if c in ' \n\r\t\v\f' nextChar() ... return EOF function readIdentifier() ident = "" while !eof c = nextChar() if c in A-Za-z0-9 ident.append(c) else return Token(Identifier, ident) // or maybe... return Identifier(ident) 

然后,您可以将解析器编写为递归下降解析器。 不要试图将词法分析器/解析器阶段合二为一,这会导致代码混乱。 (根据Parsec作者的说法,它也慢了)。

如果您要从头开始编写所有代码,我肯定会考虑使用递归的正确解析器。 在你的post中,一旦你解析了源代码,你就不会真正说明你将使用令牌流做什么。

我会建议你处理一些事情
1.您的扫描仪/词法分析器的良好设计,这将是您的解析器的源代码的标记。
2.接下来就是解析器,如果你有一个很好的源语言ebnf,解析器通常可以很好地翻译成递归的解析器。
3.您真正需要的另一个数据结构是符号表。 这可以像哈希表一样简单,也可以像表示复杂记录结构等的树结构一样复杂。我认为对于CSS,你可能介于两者之间。
4.最后,您想要处理代码生成。 你有很多选择。 对于解释器,您可能只是在解析代码时动态解释。 一个更好的方法可能是生成一个i代码,然后你可以编写一个iterpreter,甚至编写一个编译器。 当然对于.NET平台,您可以直接生成IL(可能不适用于CSS :))

作为参考,我认为你并不沉重于深层理论,我不怪你。 如果你不介意Pascal,那么获得没有复杂代码的基础知识的一个非常好的起点是Jack Crenshaw的’让我们构建一个编译器’
http://compilers.iecc.com/crenshaw/
祝你好运我相信你会喜欢这个项目。

你需要从你的BNF / EBNF编写自己的Recursive Descent Parser 。 我最近必须写自己的, 这个页面提供了很多帮助。 我不确定你的意思是“只用代码”。 你的意思是你想知道如何编写自己的递归解析器吗?

如果你想这样做,你需要先掌握你的语法。 一旦你有了EBNF / BNF,就可以很容易地从中编写解析器。

我编写解析器时所做的第一件事就是读取所有内容然后将文本标记化。 所以我基本上得到了一个我被视为堆栈的令牌数组。 为了减少从堆栈中提取值然后在不需要时将其重新打开的冗长/开销,您可以使用peek方法简单地返回堆栈中的顶部值而不会弹出它。

UPDATE

根据你的评论,我不得不从头开始在Javascript中编写一个递归下降的解析器。 您可以在这里查看解析器。 只需搜索constraints函数。 我编写了自己的tokenize函数来标记输入。 我还写了另一个便利function( peek ,我之前提到过)。 解析器在此处根据EBNF进行解析。

这花了我一点时间才弄明白,因为我写了一个解析器已经好几年了(上次我写的是在学校里!),但相信我,一旦你得到它,你就得到它。 我希望我的榜样让你继续前行。

另一个更新

我也意识到我的例子可能不是你想要的,因为你可能会使用shift-reduce解析器。 你提到现在你正在尝试编写一个tokenizer。 就我而言,我确实在Javascript中编写了自己的tokenizer。 它可能不健壮,但它足以满足我的需求。

  function tokenize(options) { var str = options.str; var delimiters = options.delimiters.split(""); var returnDelimiters = options.returnDelimiters || false; var returnEmptyTokens = options.returnEmptyTokens || false; var tokens = new Array(); var lastTokenIndex = 0; for(var i = 0; i < str.length; i++) { if(exists(delimiters, str[i])) { var token = str.substring(lastTokenIndex, i); if(token.length == 0) { if(returnEmptyTokens) { tokens.push(token); } } else { tokens.push(token); } if(returnDelimiters) { tokens.push(str[i]); } lastTokenIndex = i + 1; } } if(lastTokenIndex < str.length) { var token = str.substring(lastTokenIndex, str.length); token = token.replace(/^\s+/, "").replace(/\s+$/, ""); if(token.length == 0) { if(returnEmptyTokens) { tokens.push(token); } } else { tokens.push(token); } } return tokens; } 

基于您的代码,看起来您正在同时阅读,标记和解析 - 我假设这是一个shift-reduce解析器的作用? 我所拥有的流程首先是标记化来构建标记堆栈,然后通过递归下降解析器发送标记。

看起来您想要实现一个“shift-reduce”解析器 ,您可以在其中显式构建令牌堆栈。 通常的替代方案是“递归下降”解析器,其中过程调用的深度在实际硬件堆栈上使用它们自己的局部变量构建相同的令牌堆栈。

在shift-reduce中,术语“reduce”指的是在显式维护的令牌堆栈上执行的操作。 例如,如果堆栈的顶部已成为Term, Operator, Term则可以应用缩减规则,从而导致Expression作为模式的替代。 减少规则在shift-reduce解析器使用的数据结构中明确编码; 因此,所有缩减规则都可以在源代码的相同位置找到。

与递归下降相比,降低移位方法带来了一些好处。 在主观层面上,我的观点是,shift-reduce比递归下降更容易阅读和维护。 更客观地,shift-reduce允许在发生意外令牌时来自解析器的更多信息性错误消息。

具体来说,因为shift-reduce解析器具有用于进行“缩减”的规则的明确编码,所以解析器可以容易地扩展以清楚地表达可以合法地遵循什么类型的令牌。 (例如,“;预期”)。 递归下降实现不能轻易扩展以执行相同的操作。

一本关于两种解析器的好书,以及实现各种shift-reduce的权衡是Thomas W. Parsons编写的“编译器构造简介” 。

Shift-reduce有时被称为“自下而上”解析,递归下降有时被称为“自上而下”解析。 在所使用的类比中,具有最高优先级的节点(例如,乘法表达式中的“因子”)被认为是在解析的“底部”。 这与“递归下降”的“下降”中使用的相同类比是一致的。

如果你想使用解析器来处理格式不正确的表达式,你真的想要一个递归下降解析器。 更容易使error handling和报告可用。

对于文学,我会推荐一些Niklaus Wirth的旧作。 他知道怎么写。 算法+数据结构=程序是我使用的,但你可以在网上找到他的编译器构造 。