парсер рекурсивного спуска и функциональное программирование - PullRequest
27 голосов
/ 11 декабря 2010

Так что в последнее время я работаю над написанием простого компилятора, чтобы лучше понять концепции компилятора.Будучи прилежным читателем stackoverfolow, кажется, что есть консенсус, что написание компилятора на функциональном языке легче, чем императивный.Для этого я решил убить двух зайцев и написать компилятор на F #, чтобы одновременно выучить функциональный язык и написать компилятор.

Я читал книгу о драконах и решил:Начнем с парсера рекурсивного спуска, написанного от руки на F #.Книга драконов, однако, содержит почти все примеры кода в императивном стиле.Например, функция токена соответствия выполняет значительную часть своей работы через побочный эффект.

Итак, мой вопрос: как будет выглядеть более традиционный функциональный подход к синтаксическому анализу (т. Е. Несколько побочных эффектов)?Я знаю, что компилятор Haskell (GHC) написан на Haskell, но я был бы признателен за несколько меньший и более легкий для понимания пример кода.

Во-вторых, стоит ли пытаться применить функциональный подход к синтаксическому анализу илиэто действительно на оптимизациях к промежуточному коду, что функциональные языки сияют, и я просто еще не добрался там?То есть, следует ли мне возиться с синтаксическим анализом в F # с использованием императивного стиля и позже перейти к более функциональному подходу?

Ответы [ 5 ]

12 голосов
/ 12 декабря 2010

Ответ, полученный из этой статьи блога :

Итак, мой вопрос: как будет выглядеть более традиционный функциональный подход к синтаксическому анализу (т. Е. Несколько побочных эффектов)?

Звучит так, как будто вам нужно отделить функционал (как в Lisp, Scheme, Standard ML, CAML, OCaml, F #) от чистоты (отсутствие побочных эффектов, как в Haskell) и случайные языковые возможности (алгебраические типы данных, шаблонсопоставление).

Благодаря алгебраическим типам данных, сопоставлению с образцом и функциям более высокого порядка, F # хорош для разбора и отлично подходит для преобразований и генерации кода, но большинство производственных анализаторов, написанных на F #, не являются чистыми.Исторически семейство языков F # в основном происходит от (MetaLanguages, или MLs), созданных специально для этого типа метапрограммирования.

Вот очень простой набор взаимно-рекурсивных активных шаблонов, которые анализируют и оценивают математическиевыражения, состоящие из однозначных чисел, + - * операторов и подвыражений в скобках:

> let rec (|Term|_|) = function
    | Factor(e1, t) ->
        let rec aux e1 = function
          | '+'::Factor(e2, t) -> aux (e1 + e2) t
          | '-'::Factor(e2, t) -> aux (e1 - e2) t
          | t -> Some(e1, t)
        aux e1 t
    | _ -> None
  and (|Factor|_|) = function
    | '-'::Factor(e, t) -> Some(-e, t)
    | Atom(e1, '*'::Factor(e2, t)) -> Some(e1 * e2, t)
    | Atom(e, t) -> Some(e, t)
    | _ -> None
  and (|Atom|_|) = function
    | c::t when '0'<=c && c<='9' -> Some(int(string c), t)
    | '('::Term(e, ')'::t) -> Some(e, t)
    | _ -> None;;
val ( |Term|_| ) : char list -> (int * char list) option
val ( |Factor|_| ) : char list -> (int * char list) option
val ( |Atom|_| ) : char list -> (int * char list) option

Вот пример его использования для анализа и вычисления выражения:

> let (Term e) = List.ofSeq "1+2*(3-4)*-5";;
val e : int * char list = (11, [])

Это чисторешение, которое использует сопоставление с образцом по спискам с активными образцами F #.В действительности вы захотите определить тип для вашего абстрактного синтаксического дерева и вернуть значение этого типа.Это очень просто в F #:

type expr =
  | Int of int
  | Neg of expr
  | Add of expr * expr
  | Sub of expr * expr
  | Mul of expr * expr

  static member (~-) f = Neg f
  static member (+) (f, g) = Add(f, g)
  static member (-) (f, g) = Sub(f, g)
  static member (*) (f, g) = Mul(f, g)

let rec (|Term|_|) = function
  | Factor(e1, t) ->
      let rec aux e1 = function
        | '+'::Factor(e2, t) -> aux (e1 + e2) t
        | '-'::Factor(e2, t) -> aux (e1 - e2) t
        | t -> Some(e1, t)
      aux e1 t
  | _ -> None
and (|Factor|_|) = function
  | '-'::Factor(e, t) -> Some(-e, t)
  | Atom(e1, '*'::Factor(e2, t)) -> Some(e1 * e2, t)
  | Atom(e, t) -> Some(e, t)
  | _ -> None
and (|Atom|_|) = function
  | c::t when '0'<=c && c<='9' -> Some(Int(int(string c)), t)
  | '('::Term(e, ')'::t) -> Some(e, t)
  | _ -> None

let (Term e) = List.ofSeq "1+2*(3-4)*-5"

Обратите внимание, что требовалось только одно незначительное изменение синтаксического анализатора, поскольку AST также может быть создан с использованием операторов +, - и *.

Во-вторых, стоит ли пытаться применить функциональный подход к синтаксическому анализу, или это действительно на оптимизацию промежуточного кода, что функциональные языки сияют, и я просто еще не достиг?

Вы говорите о чистоте, а не о функциональном программировании.Чистота не особенно полезна в контексте анализа текста и, фактически, может быть реальным препятствием (например, интернирование символов - кошмар в Хаскеле).Тем не менее, F # имеет много других преимуществ, которые делают его полезным для этого набора проблем.В частности, хотя другие языки, такие как OCaml, имеют гораздо лучшие инструменты для синтаксического анализа, я думаю, что F # является лучшим языком .NET в этом контексте.

То есть, если мне придется перебирать синтаксический анализ в F #, используяимперативный стиль и перейти к более функциональному подходу позже?

Полностью зависит от того, что вы хотите сделать функциональным.Я бы использовал fslex и fsyacc с чистым кодом для конструирования AST в действиях, но нечистоты для чего-либо вроде хеширования или генерации уникальных идентификаторов.

Вы можете оценить следующие статьи на эту тему, которые я написал на этот блог (примечание paywall):

  • "Разбор текста с помощью Lex и Yacc" (30 сентября 2007 г.).
  • "Оптимизация простого интерпретатора байт-кода" (31 октября 2007 г.).
  • "Парсер-комбинаторы" (30 ноября 2007 г.).
  • "Языковое программирование: переводчик на уровне терминов" (31 декабря 2007 г.).
  • "Язык-ориентированное программирование: переопределение терминов »(16 августа 2008 г.).
  • « Генерация кода во время выполнения с использованием System.Reflection.Emit »(31 августа 2008 г.).
  • « Анализ и визуализация двоичной географической информационной системыданные "(30 ноября 2009 г.).
8 голосов
/ 11 декабря 2010

Одной из стратегий функционального синтаксического анализа являются комбинаторы монадических синтаксических анализаторов.Вы можете прочитать об этом здесь (и перейти по ссылкам) или использовать библиотеку типа FParsec .Однако я не рекомендую этот подход, если вы только изучаете / запускаете F # / компиляторы.

Другой подход с F # заключается в использовании FsLex / FsYacc (в PowerPack ).Я немного ненавижу технологию Lex / Yacc, поэтому я не рекомендую это.

Я думаю, вы должны написать рекурсивный приличный парсер вручную.У меня нет сильных чувств в отношении токенизатора, но просто токенизировать весь файл в (n неизменяемых) list токенов, а затем выполнить рекурсивное снижение (и использовать некоторое сопоставление с шаблоном) - хороший способ справиться с анализом,И, конечно же, вы захотите использовать разграниченные союзы для представления выходных данных синтаксического анализатора AST (а-ля здесь ).

Я давно не читал книгу о драконахНо я, видимо, единственный человек на планете, которому это не нравится.Я хотел бы отказаться от этого текста в пользу книги, в которой обсуждаются компиляторы, использующие некоторый язык, основанный на ML, хотя я не могу рекомендовать один случайный.

РЕДАКТИРОВАТЬ

Я не сделал ни одного изчерез некоторое время, поэтому я потратил несколько минут, чтобы закодировать небольшой пример.

// AST for tiny language
type Op = 
    | Plus 
    | Minus 
type Expr = 
    | Literal of int 
    | BinaryOp of Expr * Op * Expr // left, op, right 
type Stmt =
    | IfThenElse of Expr * Stmt * Stmt // cond, then, else; 0=false in cond 
    | Print of Expr

// sample program
let input = @"
    if 1+1-1 then 
        print 42 
    else 
        print 0"

// expected AST
let goal = 
    IfThenElse(
        BinaryOp( BinaryOp(Literal(1),Plus,Literal(1)), Minus, Literal(1)), 
        Print(Literal(42)), 
        Print(Literal(0))) 

////////////////////////////////////////////////////////////////////////////
// Lexer

type Token =
    | IF
    | THEN
    | ELSE
    | PRINT
    | NUM of int  // non-negative
    | PLUS
    | MINUS
    | EOF

let makeTokenizer (s:string) =
    let i = ref 0
    let keywords = [
        "if", IF 
        "then", THEN
        "else", ELSE
        "print", PRINT
        "+", PLUS
        "-", MINUS ]
    let rec getNextToken() =
        if !i >= s.Length then
            EOF
        elif System.Char.IsWhiteSpace(s.[!i]) then
            incr i
            getNextToken()
        elif System.Char.IsDigit(s.[!i]) then
            let mutable j = !i
            while j < s.Length && System.Char.IsDigit(s.[j]) do
                j <- j + 1
            let numStr = s.Substring(!i, j - !i)
            i := j
            NUM(System.Int32.Parse(numStr)) // may throw, e.g. if > MAXINT
        else 
            let keyword = keywords |> List.tryPick (fun (kwStr,kwTok) ->
                if s.IndexOf(kwStr, !i) = !i then
                    i := !i + kwStr.Length
                    Some(kwTok)
                else
                    None)
            match keyword with
            | Some k -> k
            | None -> 
                failwith "unexpected char '%c' at position %d" s.[!i] !i
    getNextToken

let tokens = 
    let nextToken = makeTokenizer input
    let t = ref(nextToken())
    [ 
        yield !t
        while !t <> EOF do
            t := nextToken()
            yield !t
    ]

printfn "%A" tokens // sanity check our tokenizer works

/////////////////////////////////////////////////////////////////////////
// Parser

let parseExpr toks =
    match toks with
    | NUM x :: rest ->
        let mutable rest = rest
        let mutable expr = Literal x
        while rest.Head = PLUS || rest.Head = MINUS do
            let op,y,r = 
                match rest with
                | PLUS::NUM y::t -> Plus, Literal y, t
                | MINUS::NUM y::t -> Minus, Literal y, t
                | _ -> 
                    failwith "parse error in expression, expected number"
            expr <- BinaryOp(expr, op, y)
            rest <- r
        expr, rest
    | _ -> failwith "parse error in expression, expected number"
let rec parseStmt toks =
    match toks with
    | PRINT :: rest -> 
        let e,rest = parseExpr(rest)
        Print(e), rest
    | IF :: rest ->
        let e,rest = parseExpr(rest)
        match rest with
        | THEN :: rest ->
            let s1,rest = parseStmt(rest)
            match rest with
            | ELSE :: rest ->
                let s2,rest = parseStmt(rest)
                IfThenElse(e,s1,s2), rest
            | _ -> 
                failwith "parse error after if branch, espected 'else'"
        | _ -> 
            failwith "parse error after if expression, expected 'then'"
    | _ -> failwith "parse error, expected statement"
let parseProgram toks =
    let s,rest = parseStmt toks
    match rest with
    | [EOF] -> s
    | _ -> failwith "parse error after statement, expected EOF"

let p = parseProgram tokens
printfn "%A" p
assert( p = goal )

(Надеюсь, нет вопиющих ошибок.)

6 голосов
/ 11 декабря 2010

Более простой ответ, чем другие хорошие ответы:

Анализатор на языке функций переводит поток токенов в дерево разбора и остальную часть потока токенов. То есть имеет тип

 token list -> ast * token list

Рекурсивный порядочный синтаксический анализатор обычно имеет большое количество функций такого типа, которые питаются потоком токенов и затем строят небольшую часть дерева синтаксического анализа. Вызывая их рекурсивно (рекурсивно прилично) - вы получаете то, что хотите.

Следующий шаг - использование парсеров высшего порядка: парсеры, работающие на других парсерах. Это то, что делают библиотеки синтаксического анализатора. Возможно, вы могли бы начать с простой схемы рекурсии, а затем обновить ее до комбинаторов синтаксического анализа.

6 голосов
/ 11 декабря 2010

Парсер комбинаторы действительно красивы! FParsec - очень удобная библиотека монадических синтаксических анализаторов, которую вы должны проверить. Если вы хотите начать с чего-то простого и все еще чисто функционального, вам может понравиться токенизатор / парсер из интерпретатора Scheme серии F # здесь (мой блог): http://blogs.msdn.com/b/ashleyf/archive/2010/09/24/fscheme-0-0-0.aspx

3 голосов
/ 11 декабря 2010

Я некоторое время работал над компилятором ECMAScript на F #, поэтому я в той же лодке, что и вы. Может, некоторые мои работы могут быть вам полезны. Вот простая библиотека комбинатора парсеров, над которой я работал, которую я использую в сочетании с FParsec. Это не то, где почти идеально, но это должно дать вам что-то достаточно простое для изучения, чтобы вы могли перейти к более сложным вещам. Если вы в конечном итоге используете FParsec, вы можете заметить, что многие вещи были вдохновлены этим.

module Tools =

    open System
    open System.Diagnostics
    open LazyList 

    [<Struct;DebuggerStepThrough>]
    type State<'a, 'b> (input:LazyList<'a>, data:'b) = //'
        member this.Input = input
        member this.Data = data

    type Result<'a, 'b, 'c> = //'
    | Success of 'c * State<'a, 'b>
    | Failure of list<string> * State<'a, 'b>    

    type Parser<'a, 'b, 'c> = //' 
        State<'a, 'b> -> seq<Result<'a, 'b, 'c>>

    let zero<'a, 'b, 'c> (state:State<'a, 'b>) = //'
        Seq.empty<Result<'a, 'b, 'c>>

    let item<'a, 'b> (state:State<'a, 'b>) = seq { //'
        match state.Input with
        | Cons (head, tail) ->
            yield Success(head, State (tail, state.Data))
        | Nil -> ()
    } 

    let result<'a, 'b, 'c> (value:'c) (state:State<'a, 'b>) = seq  { //'
        yield Success (value, state)
    }

    let run p i d =
        p (State(i, d)) 

    let (>>=) (m:Parser<'a, 'b, 'c>) (f:'c -> Parser<'a, 'b, 'd>) (state:State<'a, 'b>) = //'
        let rec run errors = seq {
            for r in m state do
                match r with
                | Success (v, s) ->
                    yield! f v s
                | Failure (ms, s) ->
                    yield! run (errors @ ms)
        }
        run []

    let (<|>) (l:Parser<'a, 'b, 'c>) (r:Parser<'a, 'b, 'c>) (state:State<'a, 'b>) = //'  
        let rec run p = seq {
            for result in p state do
                match result with
                | Success (_, _) ->
                    yield result
                | Failure (_, _) -> ()
        }
        Seq.append (run l) (run r)

    type ParseMonad() =        
        member this.Bind (f:Parser<'a, 'b, 'c>, g:'c -> Parser<'a, 'b, 'd>) : Parser<'a, 'b, 'd> = f >>= g //'     
        member this.Combine (f, g) = f <|> g      
        member this.Delay (f:unit -> Parser<'a, 'b, 'c>) (state:State<'a, 'b>) = f () state //'
        member this.Return x = result x
        member this.ReturnFrom p = p
        member this.Zero () = zero

    let parse = ParseMonad()

    let (|>>) (parser:Parser<'a, 'b, 'c>) (f:'c -> 'd) = parse { //'
        let! v = parser
        return f v   
    }

    let satisfy predicate = parse {
        let! value = item
        if predicate value then
            return value 
    }

    let maybe parser = parse {
        return! parser |>> Some <|> result None 
    }

    let choice (ps:seq<Parser<'a, 'b, 'c>>) (state:State<'a, 'b>) = seq { //'
        if not (LazyList.isEmpty state.Input) then
            for p in ps do
                yield! p state    
    }

    let between left right parser =
        parse {
            let! _ = left
            let! v = parser
            let! _ = right
            return v
        }

    let skip p = parse {
        let! v = p
        return ()
    }

    let many parser = 
        let rec many result = parse {
            let! v = parser
            let result = v::result
            return! many result
            return result    
        }
        many []

    let many1 parser = parse {
        let! r = many parser
        if not r.IsEmpty then
            return r
    }

    let manyFold parser start (f:_ -> _ -> _) = parse {
        let! r = many parser
        return r |> List.fold f start
    }

    let many1Fold parser start (f:_ -> _ -> _) = parse {
        let! r = many1 parser
        return r |> List.fold f start
    } 

    let isNotFollowedBy p =
        parse {
            let! v = maybe p
            match v with
            | Some _ -> ()
            | None -> return ()
        }

    let pipe2 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (f:'c -> 'd -> 'e) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            return f v1 v2
        }

    let pipe3 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (f:'c -> 'd -> 'e -> 'f) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            return f v1 v2 v3
        }

    let pipe4 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (p4:Parser<'a, 'b, 'f>) (f:'c -> 'd -> 'e -> 'f -> 'g) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            let! v4 = p4
            return f v1 v2 v3 v4
        }

    let pipe5 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (p4:Parser<'a, 'b, 'f>) (p5:Parser<'a, 'b, 'g>) (f:'c -> 'd -> 'e -> 'f -> 'g -> 'h) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            let! v4 = p4
            let! v5 = p5
            return f v1 v2 v3 v4 v5
        }

    let tuple2<'a, 'b, 'c, 'd, 'e> (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (f:'c * 'd -> 'e) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            return f (v1, v2)
        }

    let tuple3 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (f:'c * 'd * 'e -> 'f) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            return f (v1, v2, v3)
        }

    let tuple4 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (p4:Parser<'a, 'b, 'f>) (f:'c * 'd * 'e * 'f -> 'g) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            let! v4 = p4
            return f (v1, v2, v3, v4)
        }

    let tuple5 (p1:Parser<'a, 'b, 'c>) (p2:Parser<'a, 'b, 'd>) (p3:Parser<'a, 'b, 'e>) (p4:Parser<'a, 'b, 'f>) (p5:Parser<'a, 'b, 'g>) (f:'c * 'd * 'e * 'f * 'g -> 'h) = //' 
        parse {
            let! v1 = p1
            let! v2 = p2
            let! v3 = p3
            let! v4 = p4
            let! v5 = p5
            return f (v1, v2, v3, v4, v5)
        }

    let createParserRef<'a, 'b, 'c> () = //'
        let dummyParser = fun state -> failwith "a parser was not initialized"
        let r = ref dummyParser
        (fun state -> !r state), r : Parser<'a, 'b, 'c> * Parser<'a, 'b, 'c> ref //'

ПРИМЕЧАНИЕ: Вам потребуется FSharp PowerPack для типа LazyList.

Пример:

and conditionalExpressionNoIn = 
    parse {
        let! e1 = logicalORExpressionNoIn
        return! parse {
            do! skip expectQuestionMark
            let! e2 = assignmentExpression
            do! skip expectColon
            let! e3 = assignmentExpressionNoIn
            return ConditionalExpressionNoIn (e1, e2, e3)
        }
        return ConditionalExpressionNoIn (e1, SourceElement.Nil, SourceElement.Nil)
    }
...