Рекурсивный парсер f# fparsec

У меня есть следующий AST:

type Arithmetic =
    | Add

type IntFunc =
    | MovingAverage

type Formula =
    | SingleParamOp of IntFunc * int * Formula
    | ArithmeticOp of Formula * Arithmetic * Formula
    | Price

и следующий код синтаксического анализа:

module Formula =
    let private createParser =
        let pFormula, pFormulaRef = createParserForwardedToRef<Formula, unit> ()

        let pMovingAverage: Parser<Formula, unit> =
            pipe2
                (pLiteral "moving_average" >>. pLeftBracket >>. pInt .>> pComma)
                (pFormula .>> pRightBracket)
                (fun i x -> SingleParamOp(MovingAverage, i, x))

        let pPrice = pstring "price" >>. pLeftBracket >>. pRightBracket |>> (fun _ -> Price)

        let pAdd = pipe2 (pLiteral "add" >>. pLeftBracket >>. pFormula .>> pComma) (pFormula .>> pRightBracket)  (fun l r -> ArithmeticOp(l, Add, r))

        pFormulaRef.Value <-
            choice
                [ pMovingAverage
                  pPrice
                  pAdd]

        pFormula .>> eof

    let parse = createParser

он правильно анализирует эти выражения:

moving_average(3, price())
moving_average(3, moving_average(3, price()))

однако я изменил свою идею и хочу, чтобы выражения были связаны следующим образом: цена().moving_average(3).moving_average(3)

Кажется, этого гораздо сложнее достичь с помощью вышеуказанного парсера. Я пытался:

module Formula =
    let private createParser =
        let pFormula, pFormulaRef = createParserForwardedToRef<Formula, unit> ()

        let pMovingAverage: Parser<Formula, unit> =
            pipe2
                (pFormula .>> pLiteral ".")
                (pLiteral "moving_average" >>. pLeftBracket >>. pInt .>> pRightBracket)
                (fun i x -> SingleParamOp(MovingAverage, x, i))

        let pPrice = pstring "price" >>. pLeftBracket >>. pRightBracket |>> (fun _ -> Price)

        let pAdd =
            pipe2
                (pFormula .>> pLiteral ".")
                (pLiteral "add" >>. pLeftBracket >>. pFormula .>> pRightBracket)
                (fun l r -> ArithmeticOp(l, Add, r))

        pFormulaRef.Value <- choice [ pMovingAverage; pPrice; pAdd ]

        pFormula .>> eof

    let parse = createParser

но тогда, например. это выражение:

price.add(price()) results in Exit code is 134

Я предполагаю, что парсер идет слева направо, и когда я определил отдельные парсеры, нестандартно не запускать их с идентификатором (например, add/moving_average), а не запускать их с формулы (поэтому может произойти бесконечная рецессия)? Кажется, мне нужно самому справиться с рекурсией, но я не могу использовать этот парсер? или я что-то упускаю? Похоже, что левая и правая рекурсия — сложная проблема, или я что-то контролирую?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
52
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если вам нужно объединить вещи только на верхнем уровне, то синтаксический анализатор Chainl от FParsec, вероятно, будет хорошим выбором. Что-то вроде этого:

type Formula =
    | MovingAverage of int
    | Price
    | Add of Formula

module Formula =

    let pLiteral = pstring
    let pLeftBracket = pchar '('
    let pInt = pint32
    let pRightBracket = pchar ')'

    let private parseOne =
        let pFormula, pFormulaRef =
            createParserForwardedToRef<Formula, unit> ()

        let pMovingAverage: Parser<Formula, unit> =
            pLiteral "moving_average"
                >>. pLeftBracket
                >>. (pInt |>> MovingAverage)
                .>> pRightBracket

        let pPrice =
            pstring "price"
                >>. pLeftBracket
                >>. pRightBracket
                >>% Price

        let pAdd =
            pLiteral "add"
                >>. pLeftBracket
                >>. (pFormula |>> Add)
                .>> pRightBracket

        pFormulaRef.Value <- choice [ pMovingAverage; pPrice; pAdd ]

        pFormula |>> List.singleton

    let private parseDot =
        pLiteral "."
            >>% (@)

    let parse =
        chainl parseOne parseDot []

Тестируем это:

runParserOnString Formula.parse () "" "price()"
    |> printfn "%A"   // Success: [Price]

runParserOnString Formula.parse () "" "price().add(price())"
    |> printfn "%A"   // Success: [Price; Add Price]

runParserOnString Formula.parse () "" "price().moving_average(3).moving_average(3)"
    |> printfn "%A"   // Success: [Price; MovingAverage 3; MovingAverage 3]

Обратите внимание, что это не позволит вам анализировать вложенные выражения, такие как price().add(price().add(price()).

к сожалению, я бы тоже хотел их вложить. Знал о функции цепочки, проблема именно в этом, а не вложенности.

user3530206 25.05.2024 01:06
Ответ принят как подходящий

Вы правы насчет рекурсии, она действительно вызовет вашу ошибку. На самом деле, проблема именно в (pFormula .>> pLiteral "."), поскольку он будет рекурсивно вызывать ваш парсер в цикле.

Я собираюсь начать с предположения (потому что в вашем вопросе этого не написано), что:

let pLiteral = pstring
let pLeftBracket = pchar '('
let pRightBracket = pchar ')'

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

let parens p = between pLeftBracket pLeftBracket p

Далее нам нужно разобраться с вашей проблемой с парсером. Как вы указали, вы хотите объединить парсеры в цепочку, поэтому один из способов сделать это — рассматривать pMovingAverage и pAdd как парсеры, которые можно объединять в цепочку, т. е. возвращать Parser<Formula -> Formula, unit>. Мы не делаем это для Price, который (если я правильно понимаю) не цепляется с учетом вашего АСТ. Это дает нам:

let pMovingAverage: Parser<(Formula -> Formula), unit> =
    pstring "moving_average" >>. parens pint32 |>> fun x i -> SingleParamOp(MovingAverage, x, i)

и

let pAdd: Parser<Formula -> Formula, unit> =
    pstring "add" >>. parens pFormula |>> fun r l -> ArithmeticOp(l, Add, r)

Это работает, потому что лямбда-функция каждый раз ожидает дополнительный аргумент (второй), который будет указан позже. Поэтому нам необходимо определить анализатор цепочки, который объединит два предыдущих анализатора. Учитывая вашу проблему, это всегда будет следовать шаблону

"price().[a list of either pMovingAverage or pAdd]"

Поэтому,

let pChained: Parser<Formula, unit> =
    pPrice .>>. many (pchar '.' >>. choice [ pMovingAverage; pAdd ])
    |>> fun (state, fs) -> List.fold (fun acc f -> f acc) state fs

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

Мы можем это проверить:

let runParser input =
    match runParserOnString Formula.parse () null input with
    | Success(result, _, _) -> printfn "Parsed: %A" result
    | Failure(msg, _, _) -> printfn "Failure: %s" msg

runParser "price().add(price())" // Parsed: ArithmeticOp (Price, Add, Price)
runParser "price().moving_average(3).moving_average(3)" // Parsed: SingleParamOp (MovingAverage, 3, SingleParamOp (MovingAverage, 3, Price))

Полный код:

open FParsec

type Arithmetic =
    | Add

type IntFunc =
    | MovingAverage

type Formula =
    | SingleParamOp of IntFunc * int * Formula
    | ArithmeticOp of Formula * Arithmetic * Formula
    | Price

module Formula =

    let private createParser =
        let pFormula, pFormulaRef = createParserForwardedToRef<Formula, unit> ()

        let parens (p: Parser<'a, unit>) =
            between (pchar '(') (pchar ')') p

        let pPrice: Parser<Formula,unit> =
            pstring "price" >>. parens spaces >>% Price

        let pMovingAverage: Parser<(Formula -> Formula), unit> =
            pstring "moving_average" >>. parens pint32 |>> fun x i -> SingleParamOp(MovingAverage, x, i)

        let pAdd: Parser<Formula -> Formula, unit> =
            pstring "add" >>. parens pFormula |>> fun r l -> ArithmeticOp(l, Add, r)

        let pChained: Parser<Formula, unit> =
            pPrice .>>. many (pchar '.' >>. choice [ pMovingAverage; pAdd ])
            |>> fun (state, fs) -> List.fold (fun acc f -> f acc) state fs

        pFormulaRef.Value <- pChained
        pFormula .>> eof

    let parse = createParser

let runParser input =
    match runParserOnString Formula.parse () null input with
    | Success(result, _, _) -> printfn "Parsed: %A" result
    | Failure(msg, _, _) -> printfn "Failure: %s" msg

runParser "price().add(price())"
runParser "price().moving_average(3).moving_average(3)"

Есть и другие способы сделать что-то, и тот, который я представил, возможно, не самый лучший или самый простой, но это то, что пришло на ум первым (потому что я раньше делал нечто подобное, но не с помощью парсеров).

спасибо большое, думаю, у вас получилось. кроме того, хотя я не указывал это явно, ваше предложение также будет правильно анализировать вложенное правило, например -> price().moving_average(3).add(price().moving_average(3)) Не думал об определении измененного правила таким образом (изо всех сил пытался рассуждать о вложенном случае.

user3530206 25.05.2024 01:18

Определение парсеров, которые сами возвращают функции между парсерами, которые будут оцениваться позже, не является чем-то очень распространенным, и я думаю, что это довольно сложно рассуждать о больших кодах. Но здесь это работает хорошо.

Foxy 25.05.2024 01:29

@user3530206 user3530206 Когда вас устроит один из ответов на ваш вопрос, не забудьте пометить его как решенный, чтобы всем было проще. Рад, что все равно смог помочь :)

Foxy 25.05.2024 01:29

Другие вопросы по теме