如何使用 megaparsec 正确解析缩进块?

How to properly parse indented block with megaparsec?

我正在尝试制作一种基于缩进的编程语言,我正在尝试解析如下内容:

expr1 :
  expr2
  expr3

这里,本质上,:表示一个新的缩进块的开始,所以expr1是完全不相关的,想法是:可以出现在行的任何地方,而且必须是该行的最后一个标记。

我得到了或多或少有效的代码:

block :: Parser Value
block = dbg "block" $ do
  void $ symbol ":"
  void $ eol
  space1
  (L.indentBlock spaceConsumer indentedBlock)
  where
    indentedBlock = do
      e <- expr
      pure (L.IndentMany Nothing (\exprs -> pure $ Block () (e : exprs)) expr)

但问题是在示例中,只有块的第一个表达式被解析为正确的缩进,其他的必须更缩进,像这样

expr1 :
  expr2
   expr3
   expr4
   expr5

我无法提供 megaparsec 具体建议,因为我不知道那个特定的库,但是我可以通过编写一些缩进敏感语言解析器来给你我的智慧:如果你将 lex 和解析分开,你的生活会容易得多步骤并在词典分析过程中添加 indent_beginindent_end

我通常添加以下组合器:

import qualified Text.Megaparsec.Char.Lexer as L

indented :: Pos -> Parser a -> Parser (Pos, a)
indented ref p = do pos <- L.indentGuard space GT ref 
                    v <- p
                    pure (pos, v)
        

aligned :: Pos -> Parser a -> Parser a
aligned ref p = L.indentGuard space EQ ref *> p

然后你可以使用L.indentLevel来获取引用缩进。

下面是一个解析包含错误处理的语句块的示例:

blocked1 :: Pos -> Parser a -> Parser [a]
blocked1 ref p = do (pos, a) <- indented ref p
                    rest <- many (try $ helper pos)
                    fpos <- getPosition
                    rest' <- traverse (reportErrors pos) rest
                    setPosition fpos
                    pure (a : rest')
    where helper pos' = do pos <- getPosition
                           a <- p
                           when (sourceColumn pos <= ref) $ L.incorrectIndent EQ pos' (sourceColumn pos)
                           pure (pos, a)
          reportErrors ref (pos, v) = setPosition pos *>
            if ref /= sourceColumn pos
               then L.incorrectIndent EQ ref (sourceColumn pos)
               else pure v
                
blocked :: Pos -> Parser a -> Parser [a]
blocked ref p = blocked1 ref p <|> pure []

block :: Pos -> Parser (Block ParserAst)
block ref = do
       s <- blocked1 ref stmt
       pure $ Block s


funcDef :: Parser (FuncDef ParserAst)
funcDef = annotate $
    do pos <- L.indentLevel 
       symbol "def"
       h <- header
       l <- localDefs 
       b <- block pos
       pure $ FuncDef h l b

我最终在与 :

相同的位置解析了 expr1

显然 indentBlock 从解析器作为最后一个参数传递的列开始计数,所以想法是从行的开头开始解析(相对于当前缩进级别),它结束了像这样:

block :: Parser Value
block =
  L.indentBlock spaceConsumer indentedBlock
  where
    indentedBlock = do
      caller <- callerExpression
      args <- parseApplicationArgs
      pure (L.IndentSome Nothing (exprsToAppBlock caller args) parse)
    exprsToAppBlock caller args exprs =
      pure (Application () caller (args <> [Block () exprs]))