Парсер вызова функции - FParsec
Я пытаюсь разобрать вызов функции, вот варианты:
add 8 2
add x y
add (inc x) (dec y)
funcWithoutArgs
В зависимости от того, как я распределяю свои анализаторы в коде и, возможно, также как они кодируются, я получаю ошибки, а также успешные, но нежелательные анализы. Например, это:
add 4 7
возвращает следующий AST:
[Call ("foo",[Number 4]);
Number 7]
Поэтому он принимает только первый параметр.
Когда я это делаю:
foo x y
Он отправляет мне обратно это АСТ:
[Call ("foo",[Call ("x",[Call ("y",[])])])]
И это не то, что я хочу, так как здесь каждый параметр вызывает следующий параметр.
Другой пример, когда я делаю это:
foo x y
inc x
Я получил:
[Call ("foo",[Call ("x",[Call ("y",[Call ("inc",[Call ("x",[])])])])])]
Он делает то же, что и выше, но также вызывает код, следующий за строкой. Когда я спрашиваю свой анализатор о новой строке (см. Код), он отправляет мне это:
[Call ("foo",[]); Call ("x",[]); Call ("y",[]); Call ("inc",[]); Call ("x",[])]
Даже в скобках это не работает:
foo (x) (y)
Дать:
[Call ("foo",[]); Call ("x",[]); Call ("y",[])]
А также:
add (inc x) (dec y)
Дать:
Error in Ln: 1 Col: 1
Note: The error occurred on an empty line.
The parser backtracked after:
Error in Ln: 2 Col: 5
add (inc x) (dec y)
^
Expecting: end of input or integer number (32-bit, signed)
The parser backtracked after:
Error in Ln: 2 Col: 10
add (inc x) (dec y)
^
Expecting: ')'
[]
Короче говоря, мой анализатор вызовов функций не работает должным образом. Каждый раз, когда я что-то меняю, например, новую строку, попытку или другую иерархию, что-то не работает... У вас есть идеи, как решить эту очень неприятную проблему?
Вот минимальный функциональный код, который был использован:
open FParsec
// Ast
type Expression =
| Number of int
| Call of string * Expression list
type Program = Expression list
// Tools
let private bws p =
spaces >>? p .>>? spaces
let private suiteOf p =
sepEndBy p spaces1
let inline private betweenParentheses p label =
between (pstring "(") (pstring ")") p
<?> (label + " between parentheses")
let private identifier =
many1Satisfy2 isLetter (fun c -> isLetter c)
// Expressions
let rec private call = parse {
let! call = pipe2 (spaces >>? identifier) (spaces >>? parameters)
(fun id parameters -> Call(id, parameters)) // .>>? newline
return call
}
and private parameters = suiteOf expression
and private callFuncWithoutArgs =
identifier |>> fun id -> Call(id, [])
and private number = pint32 |>> Number
and private betweenParenthesesExpression =
parse { let! ex = betweenParentheses expression "expression"
return ex }
and private expression =
bws (attempt betweenParenthesesExpression <|>
attempt number <|>
attempt call <|>
callFuncWithoutArgs)
// -------------------------------
let parse code =
let parser = many expression .>>? eof
match run parser code with
| Success(result, _, _) -> result
| Failure(msg, _, _) ->
printfn "%s" msg
[]
System.Console.Clear()
parse @"
add 4 7
foo x y
inc x
foo (x) (y)
add (inc x) (dec y)
" |> printfn "%A"
1 ответ
Ваша главная проблема в том, что у вас неправильный дизайн верхнего уровня для вашего парсера.
Ваш текущий дизайн состоит в том, что выражение может быть:
- Выражение (так сказать, "подвыражение") в скобках (здесь нет проблем)
- Число (здесь нет проблем)
- Вызов с параметрами, который является идентификатором, за которым следует разделенный пробелами список выражений (это основная часть проблемы)
- Вызов без параметров, который является единым идентификатором (это усугубляет проблему)
Глядя на выражение foo x y
давайте применим эти правила по порядку, как это сделал бы парсер. Там нет скобок и foo
это не число, так что это либо 3, либо 4. Сначала мы попробуем 3. foo
сопровождается x y
: делает x y
разобрать как выражение? Почему, да, это так: он анализирует как вызов с параметрами, где x
это функция и y
это параметр. поскольку x y
соответствует 3, анализируется в соответствии с правилом 3 без проверки правила 4 и т. д. foo x y
соответствует как foo (x y)
будет: вызов foo
с одним параметром, который является вызовом x
с параметром y
,
Как это исправить? Ну, вы можете попробовать поменять местами порядок 3 и 4, чтобы перед вызовом с параметрами проверялся вызов функции без параметров (что x y
разобрать как раз x
, Но это не получится, потому что foo x y
будет соответствовать как раз foo
, Таким образом, размещение правила 4 перед правилом 3 здесь не работает.
Реальное решение состоит в том, чтобы разделить правила для выражения на два уровня. "Внутренний" уровень, который я назову "значением", может быть:
- Выражение в скобках
- Число
- Вызов функции без параметров
И "внешний" уровень, правила разбора для выражений, будет:
- Вызов функции с параметрами, которые являются значениями, а не выражениями
- Ценность
Обратите внимание, что эти уровни синтаксического анализа являются взаимно рекурсивными, поэтому вам необходимо использовать createParserForwardedToRef
в вашей реализации. Давайте посмотрим, как foo x y
будет проанализирован с этим дизайном:
Первый, foo
анализирует как идентификатор, поэтому проверьте, может ли это быть вызов функции с параметрами. Есть ли x
разбирать как значение? Да, согласно правилу 3 ценностей. И делает y
разбирать как значение? Да, согласно правилу 3 ценностей. Так foo x y
разбирает как вызов функции.
Теперь насчет funcWithoutParameters
? Это не выполнит правило 1 выражений, потому что за ним не следует список параметров. Таким образом, он будет проверен на правило 2 выражений, а затем будет соответствовать правилу 3 значений.
Хорошо, базовая проверка работоспособности псевдокода работает, поэтому давайте превратим это в код. Но сначала я упомяну другую проблему в вашем парсере, которую я еще не упомянул, а именно то, что вы не понимаете, что FParsec spaces
Парсер также соответствует символам новой строки. Поэтому, когда вы заверните expression
парсер в bws
("между пробелами"), он также будет использовать новые строки после текста, который он анализирует. Поэтому, когда вы анализируете что-то вроде:
foo a b
inc c
suiteOf expression
видит список a b inc c
и превращает все это в параметры для foo
, В моем коде ниже я различаю FParsec spaces
синтаксический анализатор (который включает новые строки) и анализатор, который анализирует только горизонтальные пробелы (пробел и табуляция, но не перевод строки), используя каждый в соответствующем месте. Следующий код реализует дизайн, который я упомянул в этом ответе, и его вывод выглядит мне правильно для всех тестовых выражений, которые вы написали:
open FParsec
// Ast
type Expression =
| Number of int
| Call of string * Expression list
type Program = Expression list
// Tools
let private justSpaces = skipMany (pchar ' ' <|> pchar '\t')
let private justSpaces1 = skipMany1 (pchar ' ' <|> pchar '\t')
let private bws p =
spaces >>? p .>>? spaces
let private suiteOf p =
sepEndBy1 p (justSpaces1)
let inline private betweenParentheses p label =
between (pstring "(") (pstring ")") p
<?> (label + " between parentheses")
let private identifier =
many1Satisfy2 isLetter (fun c -> isLetter c)
// Expressions
let private expression, expressionImpl = createParserForwardedToRef()
let private betweenParenthesesExpression =
parse { let! ex = betweenParentheses expression "expression"
return ex }
let private callFuncWithoutArgs =
(identifier |>> fun id -> Call(id, []))
let private number = pint32 |>> Number
let private value =
justSpaces >>? (attempt betweenParenthesesExpression <|>
attempt number <|>
callFuncWithoutArgs)
let private parameters = suiteOf value
let rec private callImpl = parse {
let! call = pipe2 (justSpaces >>? identifier) (justSpaces >>? parameters)
(fun id parameters -> Call(id, parameters))
return call }
let call = callImpl
expressionImpl.Value <-
bws (attempt call <|>
value)
// -------------------------------
let parse code =
let parser = many expression .>>? (spaces >>. eof)
match run parser code with
| Success(result, _, _) -> result
| Failure(msg, _, _) ->
printfn "%s" msg
[]
System.Console.Clear()
parse @"
add 4 7
foo x y
inc x
foo (x) (y)
add (inc x) (dec y)
" |> printfn "%A"
PS Я использовал следующий оператор, предложенный http://www.quanttec.com/fparsec/users-guide/debugging-a-parser.html чтобы сильно помочь мне в отслеживании проблемы:
let (<!>) (p: Parser<_,_>) label : Parser<_,_> =
fun stream ->
printfn "%A: Entering %s" stream.Position label
let reply = p stream
printfn "%A: Leaving %s (%A)" stream.Position label reply.Status
reply
Использование: поворот let parseFoo = ...
в let parseFoo = ... <!> "foo"
, Затем вы получите поток отладочной информации в консоли, который выглядит примерно так:
(Ln: 2, Col: 20): Entering expression
(Ln: 3, Col: 1): Entering call
(Ln: 3, Col: 5): Entering parameters
(Ln: 3, Col: 5): Entering bwParens
(Ln: 3, Col: 5): Leaving bwParens (Error)
(Ln: 3, Col: 5): Entering number
(Ln: 3, Col: 6): Leaving number (Ok)
(Ln: 3, Col: 7): Entering bwParens
(Ln: 3, Col: 7): Leaving bwParens (Error)
(Ln: 3, Col: 7): Entering number
(Ln: 3, Col: 8): Leaving number (Ok)
(Ln: 3, Col: 8): Leaving parameters (Ok)
(Ln: 3, Col: 8): Leaving call (Ok)
(Ln: 3, Col: 8): Leaving expression (Ok)
Это очень помогает, когда вы пытаетесь понять, почему ваш парсер не выполняет то, что вы ожидаете.