如何在Haskell中实现词法分析器和解析器

时间:2012-12-02 03:31:17

标签: haskell

我在这里得到了这段代码,它是一个用Haskell构造的命令式编程语言编写的程序,所以问题是“如何为这种语言实现词法分析器和解析器”,该程序被定义为一系列语句有6种类型:“:=”,“goto”,“write”,“stop”,“if goto”和“int”

  1. int n = 5
  2. 写n
  3. int fac = 1
  4. if0 n goto 8
  5. fac:= fac * n
  6. n:= n-1
  7. 转到4
  8. 写fac
  9. 停止
  10. 我有点迷失在这里,我读过关于词法分析器和解析器,但没有找到任何示例如何实现它们,如果你能给我一段代码我会很感激,所以我可以试着去做我自己,或者至少是有用信息的链接

1 个答案:

答案 0 :(得分:22)

使用Parsec进行解析

我不会为你写完整件事,但我会让你从每一点开始。我们要经历的三个阶段是:

  1. 定义语法
  2. 制作抽象语法树。 (这看起来像语法,所以很容易。)
  3. 编写解析器。 (这也看起来像语法,所以很容易。)
  4. 我们可以在2到3之间建立一个单独的lexing阶段,但Parsec乐意做两个级别。 Lexing是将输入分成标记的地方 - 有意义的输入位 - 相当于人类语言中的单词,也称为lexemes。跳过一个单独的lexing阶段意味着我们需要对空白等更加明确。

    1。语法

    首先,您需要定义语法。最好用纸和笔完成,但我会让你开始:

      program ::= line {[newline line]}
      line ::= num dot whitespace statement
      statement ::= declaration | write | ifstatement | goto | assignment | stop
      declaration = "Int" whitespace varname equals int
      varname = letter {[alphanum]}
      -- more things here, including the more interesting ifstatement:
      ifstatement = "if" whitespace expression whitespace equals expression whitespace statement
    
    
      -- lower level stuff:
      dot = "."
      int = digit {[digit]}
      digit = "0" | "1" | ...  | "9"
      whitespace = " " okspace | "\t" okspace
      okspace = "" | whitespace
    

    考虑一下如何匹配您的示例程序,并考虑如何完成它:

    1. Int n=5
    2. write n
    3. Int fac=1
    4. if 0 n goto 8          -- unusual
    5. fac := fac * n
    6. n := n+1               -- complex
    7. goto 4
    8. write fac
    9. stop
    

    第4行中的if语句不常见,因为其中没有===。也许这是为了简化语法,它只能接受单个变量或整数,两者之间有空格。也许这是一个错字,你的意思是有一个等号和任意表达。找出哪个并重写语法的ifstatement部分。

    第6行中的赋值很复杂,因为在这里你必须解析任意算术表达式。据我所知,有很多例子可以解决,所以我现在很乐意跳过这个例子。如果你对这个问题感到困惑,那就另外提出一个问题,但希望你能先用其余的方法建立你的解析技巧。

    2。抽象语法树(AST)

    抽象语法树表示构成输入的标记组合。在Haskell中,我们可以定义自己的内容以适应上下文,这将使生活更简单

    我实际上是compiling this answer(检查拼写错误等的好方法),这就是为什么我需要在代码顶部的一些声明:

    module ParseLang where
    
    import Text.Parsec hiding (Line)
    import Text.Parsec.String
    import Control.Applicative hiding ((<|>), many)
    

    我们只会制作一个Program Line的列表,但强制解析器必须至少有一个。

    type Program = [Line]
    

    对于Line,它需要一个数字和一个语句,但点只是我们不需要存储的语法。我们可以将行号存储为Int,因此尽管在类型声明中允许使用负数,但解析器也不会接受否定。

    data Line = Line {aNum::Int, aStatement :: Statement} 
       deriving Show
    

    多个选项很容易定义:

    data Statement = Declaration VarName Int
                   | Write VarName
                   | IfStatement Expression Expression Statement
                   | Goto LineNumber
                   | Assignment VarName Expression
                   | Stop
       deriving Show
    

    注意缺少所有语法cruft / connectives /等号只留下改变的位。

    我停在那里 - 你可以完成:

    data Expression = Expression -- I've left this one for you
       deriving Show
    
    type VarName = String   -- could use newtype for type safety for these to
    type LineNumber = Int   
    

    较低级语法不需要在AST中表示,因为我们将使用字符串。

    3。解析器

    这一点现在很简单。让我们从语法树的底部开始,然后进行处理。

    num :: Parser Int
    num = read <$> many digit
    

    我们使用了<$>,这是我们通过导入fmap获得的Control.Applicative的同义词。在这里,它使用左侧的纯函数(在本例中为read)更改解析器返回的值。如果您不习惯fmap的介绍,请查看this other answer

    让我们创建一个解析器,解析字符串文字,然后解析一些空格:

    whitespace = space >> spaces  -- a space then optional spaces
    
    lit :: String -> Parser String
    lit xs = string xs <* whitespace
    

    现在<*很有趣。它看起来像<*>,它实际上组合了两个解析器,并与<$>结合使用,它实际上将纯函数映射到结果上。 *><*组合了两个解析器,但忽略了其中一个解析器的输出,因此string "goto" <* whitespace解析了"goto"和一些空格,但抛弃了空格。

    现在我们准备解析一个goto语句:

    goto :: Parser Statement
    goto = Goto <$> (lit "goto" *> num)
    

    现在让我们来看看varName

    varName :: Parser VarName
    varName = (:) <$> letter <*> many (alphaNum <|> oneOf "'_")
    

    有几件事正在发生。

    1。 <|>是另一种选择 - 一个或另一个,所以(alphaNum <|> oneOf "'_")接受一个字母数字字符或一对无辜字符'_您可能希望包含在变量名称中。

    2。 f <$> parser1 <*> parser2是一种非常好的组合解析器的方法。它运行parser1,然后运行parser2,然后将函数f映射到它们生成的结果f上。它适用于许多解析器:

    --ifstatement = "if" whitespace expression whitespace equals whitespace expression whitespace statement
    ifStatement :: Parser Statement
    ifstatement = IfStatement 
              <$> (lit "if" >> expression) 
              <*> (lit "=" >> expression)    -- I put = in, but see below
              <*> (whitespace >> statement)  -- I'd be happier with a then in here
    

    如果您只允许VarNameInt而不是普通Expression,那么您不需要等号。

    以下是你如何把它放在一起:

    statement :: Parser Statement
    statement = goto
            <|> stop
            <|> declaration 
            <|> write
            <|> ifStatement
            <|> assignment
    
    
    --program ::= line {[newline line]}
    program :: Parser Program
    program = many1 line
    
    
    --line ::= num dot whitespace statement
    line :: Parser Line
    line = Line <$> (num <* char '.') <*> (statement <* char '\n')
    

    但是每次尝试使用尚未完成的解析器时,我都会给你留下错误信息,所以整个过程都会编译好,你定义的位应该有效。

    stop =        error "You've not yet defined stop"
    declaration = error "You've not yet defined declaration"
    write =       error "You've not yet defined write"
    ifStatement = error "You've not yet defined ifStatement"
    assignment =  error "You've not yet defined assignment"
    expression =  error "You've not yet defined expression"