Изборът на FParsec се държи по неочакван начин

Планирам да използвам FParsec за прототипа на по-голям мой проект. Затова реших да получа първия си опит с тази библиотека с помощта на тестовата програма, посочена по-долу. Но изглежда, че комбинацията от моите основни парсери (които изглежда работят) с помощта на функцията fparsec 'choice' води до неочаквано поведение.

По принцип целта е целият този прост код на анализатора на калкулатора винаги да връща сума от продукти на числа или подизрази. Подизразите от своя страна ще имат същата структура като целия израз.

Както разбрах от документацията на 'choice', алтернативите се опитват отляво надясно, както е посочено в списъка с анализатори, дадени на 'choice'. Разбрах, че ако анализатор, който е по-наляво в списъка, се провали, но е консумирал вход, следващите анализатори няма да бъдат опитани.

И все пак изглежда има нещо повече, отколкото мога да разбера в момента, сякаш беше както казах по-горе, кодът трябва да работи. Но не става.

Ще бъда много благодарен, ако някой може да ми обясни а) какво се обърка и защо и б) как да го поправя.

В основния си проект планирам да изчисля парсери от някакъв вход и затова трябва да разбера как точно да комбинирам анализатори по надежден начин без изненади.

(*
    SimpleAOSCalculator

    Should implement the following grammar:

    SimpleAOSCalculator := SUM
    SUM := SUMMAND [ '+' SUMMAND ]*
    SUMMAND := PRODUCT | SUBEXPR
    PRODUCT := FACTOR [ '*' FACTOR ]*
    FACTOR := NUMBER | SUBEXPR
    SUBEXPR := '(' SUM ')'
    NUMBER := pfloat
*)

// NOTE: If you try this in fsi, you have to change the 2 lines below to point to the spot you have your fparsec dlls stored at.
#r @"C:\hgprojects\fparsec\Build\VS11\bin\Debug\FParsecCS.dll"
#r @"C:\hgprojects\fparsec\Build\VS11\bin\Debug\FParsec.dll"

open FParsec

let testParser p input =
    match run p input with
    | Success(result, _, _) -> printfn "Success: %A" result
    | Failure(errorMsg, _, _) -> printfn "Failure %s" errorMsg
    input

type Node = 
    | Sum of SumNode
    | Product of ProductNode
    | Number of NumberNode
    | SubExpression of SubExpressionNode
and SumNode = 
    {
        Summands : Node list
    }
and ProductNode = 
    {
        Factors : Node list
    }
and NumberNode =
    {
        Value : float
    }
and SubExpressionNode =
    {
        N : Node
    }

let CreateSubExpression (n : Node) : Node =
    let s : SubExpressionNode = { N = n }
    SubExpression  s

let (PrimitiveAOSCalculator : Parser<Node,unit>), (PrimitiveAOSCalculatorImpl : Parser<Node,unit> ref) = createParserForwardedToRef()

let SubExpression : Parser<Node,unit> =
    between (pchar '(') (pchar ')') PrimitiveAOSCalculator |>> CreateSubExpression

let Number : Parser<Node,unit> =
   pfloat |>> (fun v -> Number { Value = v })

let Product : Parser<Node,unit> = 
    let Factor : Parser<Node,unit> = choice [Number; SubExpression]
    let Mult = spaces >>. pchar '*' .>> spaces
    sepBy1 Factor Mult |>> (fun l -> Product { Factors = l})

let Summand : Parser<Node,unit> =
    choice [ attempt Product; attempt SubExpression ]

let Sum = 
    let Add = (spaces >>. pchar '+' .>> spaces)
    sepBy1 Summand Add |>> (fun l -> Sum { Summands = l })

do PrimitiveAOSCalculatorImpl :=
    Sum

let rec Eval (n : Node) : float =
    match n with
    | Number(v) -> v.Value
    | Product(p) -> List.map (fun n -> Eval n) p.Factors |> List.fold (fun a b -> a * b) 1.0
    | Sum(s) -> List.map (fun t -> Eval t) s.Summands |> List.fold (fun a b -> a + b) 0.0
    | SubExpression(x) -> Eval x.N


let Calculate (term : string) : float =
    let parseResult = run PrimitiveAOSCalculator term
    match parseResult with
    | Success(ast,_,_) -> Eval ast
    | Failure(errorMessage,_,_) -> failwith ("Parsing of the expression failed: " + errorMessage)

let Show (s : string) : string =
    printfn "%s" s
    s

let test p i =
    testParser p i |> Show |> Calculate |> printfn "result = %f"

do test Product "5.1 * 2" 
do test Product "5.1"
do test Product "5.1"
do test Sum "(4 * 3) + (5 * 2)"
do test Sum "4 * 3 + 5 * 2"

do test PrimitiveAOSCalculator "42"
do test PrimitiveAOSCalculator "42 * 42"
do test PrimitiveAOSCalculator "42 + 42"
do test PrimitiveAOSCalculator "42 * 42 + 47.11"
do test PrimitiveAOSCalculator "5.1 * (32 + 88 * 3) + 1.4"

Тук $do test Sum "4 * 3 + 5 * 2" се проваля със следния изход:

Failure Error in Ln: 1 Col: 1
4 * 3 + 5 * 2
^
Expecting: '('

The parser backtracked after:
  Error in Ln: 1 Col: 7
  4 * 3 + 5 * 2
        ^
  Expecting: '*'

4 * 3 + 5 * 2
System.Exception: Parsing of the expression failed: Error in Ln: 1 Col: 1
4 * 3 + 5 * 2
^
Expecting: '('

The parser backtracked after:
  Error in Ln: 1 Col: 7
  4 * 3 + 5 * 2
        ^
  Expecting: '*'

И нямам дори най-мъглявата представа защо би очаквал "*" тук.


person BitTickler    schedule 25.05.2014    source източник
comment
Не съм тествал вашия (много голям) код, но Product изглежда грешен, тъй като използва sepBy1 и по този начин не позволява обработката на един Number. Още нещо: защо направихте синтактичен анализ и анализ на синтактично дърво в един код? Няма ли да е по-лесно да анализирате string в Element list, където type Element = | Number of int | Operation of char и след това да стартирате синтактичен анализ? Представете си колко трудно би било да добавите числово деление към вашата съществуваща кодова база.   -  person bytebuster    schedule 26.05.2014
comment
@bytebuster Грешно ли разбирам документацията? sepBy1 Factor Mult би означавало в EBNF: Factor [ Mult Factor ]* докато sepBy Factor Mult би означавало в EBNF: [Factor [ '*' Factor ]*]. Структурата на това (само 150 реда ;) ) приложение е както предлагате. SimpleAOSParser произвежда AST (дърво на възли), Eval оценява AST.   -  person BitTickler    schedule 26.05.2014


Отговори (1)


Основната грешка, която се прави много често, когато се започва с комбинатори на анализатори, е, че те не са директно еквивалентни на EBNF. Фундаменталната разлика е, че когато дадете на parsec избор, той ги пробва по ред и щом един от изборите съвпадне дори с един символ, той остава в този клон. Той ще се върне назад само ако поставите своя избор в attempt и трябва да правите това възможно най-малко (от съображения за производителност, а също и от съображения за докладване на грешки -- вижте последния ми параграф).

По-конкретно във вашия код, грешката е във вашите разделители. Комбинатори като sepBy1 са изградени от избори. Когато съвпадне с елемент, той се опитва да съпостави разделител. В този случай разделителят е spaces >>. pchar '*' .>> spaces. Тъй като spaces съвпада успешно и консумира символ, той няма да се върне обратно, дори ако pchar '*' след това се провали; той просто ще счита този анализатор като цяло за провал. Това е много често срещан проблем по отношение на празното пространство с комбинаторите на анализатора. Обичайният начин да се коригира това е винаги да се анализира празното пространство като суфикс на друг анализатор, а не като префикс. Във вашия случай трябва:

  • Заменете pfloat в Number с pfloat .>> spaces.

  • Премахнете префикса spaces >>. във вашите разделители.

  • Вероятно също така искате да добавите суфикс .>> spaces както към отварящите, така и към затварящите парсери.

Можете да напишете междинни функции, които ще предотвратят това да стане твърде многословно:

// ...

let sp parser = parser .>> spaces

let spchar c = sp (pchar c)

let SubExpression : Parser<Node,unit> =
    between (spchar '(') (spchar ')') PrimitiveAOSCalculator |>> CreateSubExpression

let Number : Parser<Node,unit> =
    sp pfloat |>> (fun v -> Number { Value = v })

let Product : Parser<Node,unit> = 
    let Factor : Parser<Node,unit> = choice [Number; SubExpression]
    let Mult = spchar '*'
    sepBy1 Factor Mult |>> (fun l -> Product { Factors = l})

let Summand : Parser<Node,unit> =
    choice [ Product; SubExpression ]

let Sum = 
    let Add = spchar '+'
    sepBy1 Summand Add |>> (fun l -> Sum { Summands = l })

// ...

Премахнах и обажданията до attempt в Summand. Те са причината вашите грешки да се появяват на такива странни места: когато анализаторът на разделителя се провали, грешката се разпространи нагоре, докато достигна до извикването на attempt Product; това attempt превърна грешката в просто "няма съвпадение и не е консумиран вход", така че изборът след това опита SubExpression вместо да се провали напълно. Това в крайна сметка ви каза, че очаква '(', въпреки че първоначалната грешка всъщност беше някъде другаде. Като правило трябва да избягвате attempt и ако наистина имате нужда от него, извикайте го на възможно най-малкия анализатор.

person Tarmil    schedule 26.05.2014
comment
Благодаря много за добрия отговор! Добавих опитните изрази в Summand с надеждата, че тогава изборът ще работи, както очаквах да работи, надявайки се, че ако един от изборите се провали, системата се връща назад и като такъв изборът продължава със следващия избор. Но по някакъв начин опитът - както посочвате - не успява да свърши тази работа. - person BitTickler; 26.05.2014