如何用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

,

我最终在与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]))
,

我通常添加以下组合器:

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