Как написать рекурсивный парсер спуска с нуля?
Как чисто академическое упражнение, я пишу рекурсивный парсер спуска с нуля - без использования ANTLR или lex/yacc.
Я пишу простую функцию, которая преобразует математические выражения в их эквивалентные АСТ. У меня есть следующее:
// grammar
type expr =
| Lit of float
| Add of expr * expr
| Mul of expr * expr
| Div of expr * expr
| Sub of expr * expr
// tokens
type tokens =
| Num of float
| LParen | RParen
| XPlus | XStar | XMinus | XSlash
let tokenize (input : string) =
Regex.Matches(input.Replace(" ", ""), "\d+|[+/*\-()]")
|> Seq.cast<Match>
|> Seq.map (fun x -> x.Value)
|> Seq.map (function
| "+" -> XPlus
| "-" -> XMinus
| "/" -> XSlash
| "*" -> XStar
| "(" -> LParen
| ")" -> RParen
| num -> Num(float num))
|> Seq.to_list
Итак, tokenize "10 * (4 + 5) - 1"
возвращает следующий токен:
[Num 10.0; XStar; LParen; Num 4.0; XPlus; Num 5.0; RParen; XMinus; Num 1.0]
В этот момент я хотел бы сопоставить токен потока с его AST по отношению к приоритету оператора:
Sub(
Mul(
Lit 10.0
,Add(Lit 4.0, Lit 5.0)
)
,Lit 1.0
)
Однако я рисую пробел. Я никогда не писал парсер с нуля, и я даже не знаю, как начать.
Как преобразовать поток токенов в его представитель AST?
Ответы
Ответ 1
Знаете ли вы о языковых грамматиках?
Предполагая, что у вас есть грамматика с правилами по строкам
...
addTerm := mulTerm addOp addTerm
| mulTerm
addOp := XPlus | XMinus
mulTerm := litOrParen mulOp mulTerm
| litOrParen
...
который заканчивается превращением в код, подобный (написание кода в браузере, никогда не скомпилированный)
let rec AddTerm() =
let mulTerm = MulTerm() // will parse next mul term (error if fails to parse)
match TryAddOp with // peek ahead in token stream to try parse
| None -> mulTerm // next token was not prefix for addOp rule, stop here
| Some(ao) -> // did parse an addOp
let rhsMulTerm = MulTerm()
match ao with
| XPlus -> Add(mulTerm, rhsMulTerm)
| XMinus -> Sub(mulTerm, rhsMulTerm)
and TryAddOp() =
let next = tokens.Peek()
match next with
| XPlus | XMinus ->
tokens.ConsumeNext()
Some(next)
| _ -> None
...
Надеюсь, вы увидите основную идею. Это предполагает глобальный измененный поток токенов, который позволяет "заглянуть в следующий токен" и "потреблять следующий токен".
Ответ 2
Если я помню из классов колледжа, идея заключалась в том, чтобы строить деревья выражений, например:
<program> --> <expression> <op> <expression> | <expression>
<expression> --> (<expression>) | <constant>
<op> --> * | - | + | /
<constant> --> <constant><constant> | [0-9]
после того как вы полностью построите свое дерево, чтобы получить что-то вроде:
exp
exp op exp
5 + and so on
то вы запустите свое завершенное дерево через другую программу, которая рекурсивно спускается в дерево, вычисляя выражения, пока не получите ответ. Если ваш парсер не понимает дерево, у вас есть синтаксическая ошибка. Надеюсь, что это поможет.