我已经编写了一个可以解决这个问题的生成器,但我想知道实现不合规则的最佳方法。
很快:Off-side rule在这种情况下意味着缩进被识别为语法元素。
以下是伪代码的越位规则,用于制作以可用形式捕获缩进的标记器,我不想按语言限制答案:
token NEWLINE
matches r"\n\ *"
increase line count
pick up and store the indentation level
remember to also record the current level of parenthesis
procedure layout tokens
level = stack of indentation levels
push 0 to level
last_newline = none
per each token
if it is NEWLINE put it to last_newline and get next token
if last_newline contains something
extract new_level and parenthesis_count from last_newline
- if newline was inside parentheses, do nothing
- if new_level > level.top
push new_level to level
emit last_newline as INDENT token and clear last_newline
- if new_level == level.top
emit last_newline and clear last_newline
- otherwise
while new_level < level.top
pop from level
if new_level > level.top
freak out, indentation is broken.
emit last_newline as DEDENT token
clear last_newline
emit token
while level.top != 0
emit token as DEDENT token
pop from level
comments are ignored before they are getting into the layouter
layouter lies between a lexer and a parser
此布局在时间上不会生成多个NEWLINE,并且在出现缩进时不会生成NEWLINE。因此,解析规则仍然非常简单。我认为这很好,但是告知是否有更好的方法来实现它。
在使用这段时间之后,我注意到在DEDENT之后无论如何都可以发出新行,这样你就可以将表达式与NEWLINE分开,同时将INDENT DEDENT保留为表达式的预告片。
答案 0 :(得分:8)
在过去的几年里,我已经为几个以缩进为中心的特定领域的语言编写了标记器和解析器,而且你所拥有的东西看起来对我来说非常合理,无论那些价值多少。如果我没有弄错的话,你的方法与Python的方法非常相似,例如,它似乎应该承担一定的重量。
将NEWLINE NEWLINE INDENT转换为INDENT之前,它确实是正确的做事方式 - 在解析器中始终向前窥视是一种痛苦(IME)!我实际上已经完成了这个步骤作为一个单独的层,结果是一个三步过程:第一步结合你的词法分析器和外行设备减去所有NEWLINE前瞻性的东西(这使得它非常简单),第二个(也非常简单) )图层折叠连续NEWLINE并将NEWLINE INDENT转换为INDENT(或实际上,COLON NEWLINE INDENT到INDENT,因为在这种情况下所有缩进的块总是以冒号开头),然后解析器是第三个阶段。但是对我来说,以你描述它们的方式做事情也很有意义,特别是如果你想将词法分析器与路由器分开,如果你使用代码生成工具,你可能会想要这样做例如,通常的做法是制作你的词法分析器。
我确实有一个应用程序需要对缩进规则更加灵活,基本上让解析器在需要时强制执行 - 以下内容需要在某些上下文中有效,例如:
this line introduces an indented block of literal text:
this line of the block is indented four spaces
but this line is only indented two spaces
对于INDENT / DEDENT令牌不能很好地工作,因为你最终需要为每一段缩进生成一个INDENT,并且在回来的路上需要相同数量的DEDENT,除非你展望未来缩进级别将最终成为现实,这似乎不是你想要一个标记器。在那种情况下,我尝试了一些不同的东西,最后只是在每个NEWLINE令牌中存储一个计数器,该计数器给出了后续逻辑行的缩进(正或负)的变化。 (每个标记还存储所有尾随空格,以防需要保留;对于NEWLINE,存储的空白包括EOL本身,任何插入的空白行和下一个逻辑行上的缩进。)根本没有单独的INDENT或DEDENT标记。让解析器处理这个问题比嵌套INDENTs和DEDENTs要多得多,而且很可能是一个需要花哨的解析器生成器的复杂语法,但它并不像我担心的那么糟糕,无论是。同样,解析器无需向前看NEWLINE以查看此方案中是否存在INDENT。
尽管如此,我认为你同意在tokenizer / layouter中允许并保留各种疯狂的空白,并让解析器决定什么是文字,什么代码是一个不寻常的要求!例如,如果您只是想解析Python代码,那么您当然不希望您的解析器背负该缩进计数器。你做事的方式几乎肯定是你的应用程序和其他许多其他方法的正确方法。虽然如果有其他人想到如何最好地做这类事情,我显然很想听听他们......
答案 1 :(得分:3)
我最近一直在试验这个,我得出的结论是,至少对于我的需求,我希望NEWLINES标记每个“语句”的结尾,无论它是否是缩进块中的最后一个语句,即在DEDENT之前我需要换行。
我的解决方案是将它转过头来,而不是标记行尾的NEWLINES,我使用LINE标记来标记一行的开头。
我有一个lexer折叠空行(包括仅注释行)并发出一个LINE标记,其中包含有关最后一行缩进的信息。然后我的预处理函数接受此标记流,并在缩进更改的任何行之间添加“介于其间”或“DEDENT”。所以
line1
line2
line3
line4
会给出令牌流
LINE "line1" INDENT LINE "line2" LINE "line3" DEDENT LINE "line4" EOF
这允许我为语句编写清晰的语法产生,而不用担心检测语句的结尾,即使它们以嵌套的,缩进的子块结束,如果你匹配NEWLINES(和DEDENTS),这可能很难。
这是预处理器的核心,用O'Caml编写:
match next_token () with
LINE indentation ->
if indentation > !current_indentation then
(
Stack.push !current_indentation indentation_stack;
current_indentation := indentation;
INDENT
)
else if indentation < !current_indentation then
(
let prev = Stack.pop indentation_stack in
if indentation > prev then
(
current_indentation := indentation;
BAD_DEDENT
)
else
(
current_indentation := prev;
DEDENT
)
)
else (* indentation = !current_indentation *)
let token = remove_next_token () in
if next_token () = EOF then
remove_next_token ()
else
token
| _ ->
remove_next_token ()
我还没有添加对括号的支持,但这应该是一个简单的扩展。但它确实避免在文件末尾发出一个迷路LINE。
答案 2 :(得分:1)
ruby中的Tokenizer非常有趣:
def tokenize(input)
result, prev_indent, curr_indent, line = [""], 0, 0, ""
line_started = false
input.each_char do |char|
case char
when ' '
if line_started
# Content already started, add it.
line << char
else
# No content yet, just count.
curr_indent += 1
end
when "\n"
result.last << line + "\n"
curr_indent, line = 0, ""
line_started = false
else
# Check if we are at the first non-space character.
unless line_started
# Insert indent and dedent tokens if indentation changed.
if prev_indent > curr_indent
# 2 spaces dedentation
((prev_indent - curr_indent) / 2).times do
result << :DEDENT
end
result << ""
elsif prev_indent < curr_indent
result << :INDENT
result << ""
end
prev_indent = curr_indent
end
# Mark line as started and add char to line.
line_started = true; line << char
end
end
result
end
仅适用于双空格缩进。结果类似于["Hello there from level 0\n", :INDENT, "This\nis level\ntwo\n", :DEDENT, "This is level0 again\n"]
。