11

我已经编写了一个可以解决问题的生成器,但我想知道实现越位规则的最佳方法。

很快:越位规则在这种情况下意味着缩进被识别为句法元素。

这是伪代码中的越位规则,用于制作以可用形式捕获缩进的标记器,我不想通过语言限制答案:

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 作为表达式的预告片。

4

3 回答 3

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 标记。让解析器处理这个问题比仅仅嵌套 INDENT 和 DEDENT 需要更多的工作,而且很可能会遇到需要花哨的解析器生成器的复杂语法,但它并没有我担心的那么糟糕,任何一个。同样,解析器不需要从 NEWLINE 向前看,看看这个方案中是否有一个 INDENT 出现。

尽管如此,我认为您会同意在标记器/布局器中允许和保留各种看起来很疯狂的空白,并让解析器决定什么是文字和什么是代码,这是一个不寻常的要求!例如,如果您只想能够解析 Python 代码,您当然不希望您的解析器背负缩进计数器。您做事的方式几乎肯定是您的应用程序和许多其他应用程序的正确方法。虽然如果其他人对如何最好地做这种事情有想法,我显然很想听听他们......

于 2008-11-03T08:59:09.043 回答
3

我最近一直在尝试这个,我得出的结论是,至少为了我的需要,我希望 NEWLINES 标记每个“语句”的结尾,无论它是否是缩进块中的最后一个语句,即我甚至在 DEDENT 之前也需要换行符。

我的解决方案是把它转过来,而不是 NEWLINES 标记行的结尾,我使用 LINE 标记来标记行的开头。

我有一个词法分析器,它可以折叠空行(包括仅注释行)并发出单个 LINE 标记,其中包含有关最后一行缩进的信息。然后我的预处理函数采用这个令牌流并在缩进发生变化的任何行之间添加 INDENT 或 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。

于 2009-06-03T18:34:32.433 回答
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"].

于 2011-05-14T15:18:47.747 回答