【问题标题】:How would you go about implementing off-side rule?您将如何实施越位规则?
【发布时间】:2010-09-18 23:04:46
【问题描述】:

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

简而言之: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 作为表达式的预告片。

【问题讨论】:

    标签: programming-languages language-features language-design lexical-analysis


    【解决方案1】:

    在过去的几年里,我为一些以缩进为中心的领域特定语言编写了分词器和解析器,无论它们值多少钱,你所拥有的对我来说都非常合理。如果我没记错的话,你的方法与 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 代码,您当然不希望您的解析器背负缩进计数器。您做事的方式几乎可以肯定是您的应用程序和许多其他应用程序的正确方法。虽然如果其他人对如何最好地做这种事情有想法,我显然很想听听他们......

    【讨论】:

      【解决方案2】:

      我最近一直在尝试这个,我得出的结论是,至少为了我的需要,我希望 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。

      【讨论】:

      • 您的代码无法发出多个 DEDENT,也不会在 EOF 之前考虑 dedent。它可能对某些东西有用,但这些东西比括号支持更重要。
      • 另外,不要担心对括号的特殊支持,你会错过最好的点,就像 python 一样。布局的重点是允许您提供出色的多行语法,它不会与括号冲突,除非您无法将两者结合起来。
      • 我的代码确实发出了多个 DEDENT,所以我认为您误读了它。但我同意我想要看起来更像 Haskell 而不是 Python 的东西,所以我需要一种新方法。
      • [我在 Haskell 中写了一个有趣的东西,][1] 使用 Parsec,一种嵌入式领域特定语言。它大约有 30 行代码,并且有完整的注释。 [1]:refactory.org/s/indentation_based_syntax_parser_tokenizer/view/…
      【解决方案3】:

      为了好玩而使用 ruby​​ 中的标记器:

      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"]

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-04-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多