问题 您将如何实施违规规则?


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

不久: 偏离规则 意味着在这种情况下,缩进被识别为语法元素。

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

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


1237
2017-10-24 06:59


起源



答案:


在过去的几年里,我已经为几个以缩进为中心的特定领域的语言编写了标记器和解析器,而且你所拥有的东西对我而言看起来非常合理,不管它有什么价值。如果我没有弄错的话,你的方法与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代码,那么您当然不希望您的解析器背负该缩进计数器。你做事的方式几乎肯定是你的应用程序和其他许多其他方法的正确方法。虽然如果有其他人有关于如何最好地做这种事情的想法,我显然喜欢听他们......


8
2017-11-03 08:59





我最近一直在试验这个,我得出的结论是,至少对于我的需求,我希望NEWLINES标记每个“语句”的结尾,无论它是否是缩进块中的最后一个语句,即我甚至在DEDENT之前就需要新行。

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

我有一个词法分析器折叠空行(包括仅注释行)并发出一个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。


3
2018-06-03 18:34



您的代码无法发出多个DEDENT,也不会在EOF之前考虑dedent。它对某些东西很有用,但这些东西比括号支持更重要。 - Cheery
另外,不要为括号的特殊支持而烦恼,你将会错过最佳点,就像python一样。布局的目的是允许您提供出色的多行语法,它不会与括号冲突,除非您无法将这两者结合起来。 - Cheery
我的代码会发出多个DEDENT,所以我认为你误读了它。但我同意我想要的东西看起来更像Haskell而不是Python,所以我需要一种新的方法。 - dkagedal
[我在Haskell中写了一个很有趣的,] [1]使用Parsec,一种嵌入式域特定语言。这是大约30行代码并完全评论。 [1]: refactory.org/s/indentation_based_syntax_parser_tokenizer/view/... - Christopher Done


答案:


在过去的几年里,我已经为几个以缩进为中心的特定领域的语言编写了标记器和解析器,而且你所拥有的东西对我而言看起来非常合理,不管它有什么价值。如果我没有弄错的话,你的方法与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代码,那么您当然不希望您的解析器背负该缩进计数器。你做事的方式几乎肯定是你的应用程序和其他许多其他方法的正确方法。虽然如果有其他人有关于如何最好地做这种事情的想法,我显然喜欢听他们......


8
2017-11-03 08:59





我最近一直在试验这个,我得出的结论是,至少对于我的需求,我希望NEWLINES标记每个“语句”的结尾,无论它是否是缩进块中的最后一个语句,即我甚至在DEDENT之前就需要新行。

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

我有一个词法分析器折叠空行(包括仅注释行)并发出一个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。


3
2018-06-03 18:34



您的代码无法发出多个DEDENT,也不会在EOF之前考虑dedent。它对某些东西很有用,但这些东西比括号支持更重要。 - Cheery
另外,不要为括号的特殊支持而烦恼,你将会错过最佳点,就像python一样。布局的目的是允许您提供出色的多行语法,它不会与括号冲突,除非您无法将这两者结合起来。 - Cheery
我的代码会发出多个DEDENT,所以我认为你误读了它。但我同意我想要的东西看起来更像Haskell而不是Python,所以我需要一种新的方法。 - dkagedal
[我在Haskell中写了一个很有趣的,] [1]使用Parsec,一种嵌入式域特定语言。这是大约30行代码并完全评论。 [1]: refactory.org/s/indentation_based_syntax_parser_tokenizer/view/... - Christopher Done


红宝石中的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"]


1
2018-05-14 15:18