Uma pequena biblioteca de combinadores de parser para Bern. Construa parsers grandes colando pequenos - os nomes centrais seguem o megaparsec do Haskell.
import vendor/bernparsec
Um parser é uma pequena receita para ler texto. Você constrói parsers grandes a partir de pequenos usando combinadores - funções que recebem parsers e devolvem um novo parser maior. Um parser não é uma função; é um objeto comum que descreve o que ler, e um único interpretador percorre essa descrição contra a entrada. Isso torna os parsers baratos de construir, fáceis de imprimir e componíveis.
import vendor/bernparsec
greeting = before(string("hello"), eof()) -- "hello" e então fim da entrada
parseTest(greeting, "hello")
-- Saída: "hello"
Todo combinador nesta página vem em dois sabores: o nome central conciso (usado ao longo do texto) e um alias amigável em inglês claro (listado na tabela de aliases). Eles são intercambiáveis - misture o que ler melhor.
Rodar um parser contra a entrada produz um objeto de resultado:
| Campo | Significado |
|---|---|
result["ok"] | true se o parser casou |
result["value"] | o valor parseado (quando ok) |
result["state"] | onde o parsing parou - index, line, column |
result["expected"] | lista do que era esperado (quando falhou) |
result["message"] | um erro legível (quando falhou) |
Roda um parser, rotulando a entrada como nome_fonte para mensagens de erro. (run é o alias amigável.)
r = parse(string("hi"), "greeting.txt", "hi there")
r["ok"]
-- Saída: true
r["value"]
-- Saída: "hi"
Roda um parser e devolve o valor parseado no sucesso, ou uma string de erro formatada na falha - perfeito para o REPL. (Alias: quickParse.)
parseTest(char('a'), "abc")
-- Saída: 'a'
parseTest(char('a'), "xyz")
-- Saída: ":1:1: unexpected input; expected: 'a'"
Formata qualquer resultado como uma string de uma linha fonte:linha:coluna: mensagem; expected: …. (Alias: prettyError.)
r = parse(char('a'), "in", "zzz")
errorBundlePretty(r)
-- Saída: "in:1:1: unexpected input; expected: 'a'"
Casa exatamente o caractere c.
parseTest(char('a'), "abc")
-- Saída: 'a'
Casa o texto exato token.
parseTest(string("let"), "let x = 1")
-- Saída: "let"
Casa qualquer caractere único.
parseTest(anySingle(), "Z!") -- Saída: 'Z'
Casa qualquer caractere que apareça em chars.
parseTest(oneOf("+-*/"), "*3")
-- Saída: '*'
Casa qualquer caractere que não apareça em chars.
parseTest(noneOf(" \t\n"), "hi")
-- Saída: 'h'
Casa um caractere para o qual pred(c) é true. O label opcional o nomeia nas mensagens de erro.
parseTest(satisfy(\c -> c == 'x', "um x"), "xyz") -- Saída: 'x'
Tem sucesso apenas no fim da entrada (não consome nada).
parseTest(before(string("ok"), eof()), "ok")
-- Saída: "ok"
parseTest(before(string("ok"), eof()), "okay")
-- Saída: (erro: esperado fim da entrada)
Sempre tem sucesso, produzindo valor sem consumir entrada. (Alias: succeed.)
parseTest(pure(42), "qualquer coisa") -- Saída: 42
Sempre falha com mensagem. (Alias: failWith.)
parseTest(fail_parser("nope"), "abc")
-- Saída: ":1:1: nope; expected: "
Parsers satisfy prontos para classes comuns, cada um com um rótulo de erro sensato.
Um dígito 0–9.
parseTest(digitChar(), "7x") -- Saída: '7'
Uma letra a–z / A–Z.
parseTest(letterChar(), "Bern") -- Saída: 'B'
Uma letra ou dígito.
parseTest(some(alphaNumChar()), "id42 ") -- Saída: ['i', 'd', '4', '2']
Um caractere de espaço em branco, uma quebra de linha ou um tab, respectivamente.
parseTest(spaceChar(), " x") -- Saída: ' '
space() casa zero ou mais espaços em branco; space1() exige pelo menos um.
parseTest(space(), " hi") -- Saída: [' ', ' ', ' ']
Roda parser e transforma seu resultado com mapper. (Alias: mapResult.)
parseTest(mapP(digitChar(), char_to_digit), "9") -- Saída: 9 (o inteiro, não o caractere)
Roda os dois em ordem, mantém o valor da direita. (Alias: keepRight.)
parseTest(thenP(char('$'), decimal()), "$50")
-- Saída: 50 (o '$' é consumido mas descartado)
Roda os dois em ordem, mantém o valor da esquerda. (Alias: keepLeft.)
parseTest(before(decimal(), char('%')), "75%")
-- Saída: 75
Sequência dependente do resultado: roda parser e passa seu valor a to_parser, que devolve o próximo parser a rodar. (Alias: andThen.)
-- lê um dígito n e então exatamente esse tanto de letras repeated = bind(mapP(digitChar(), char_to_digit), \n -> count(n, letterChar())) parseTest(repeated, "3abc") -- Saída: ['a', 'b', 'c']
Tenta esquerda; se falhar, tenta direita. (Alias: orTry.)
yesNo = orElse(string("yes"), string("no"))
parseTest(yesNo, "no")
-- Saída: "no"
Tenta cada parser da lista, devolvendo o primeiro que casar. (Alias: firstOf.)
keyword = choice([string("if"), string("else"), string("end")])
parseTest(keyword, "else ...")
-- Saída: "else"
Dá ao parser um nome amigável para mensagens de erro. (Alias: describe.)
p = label(digitChar(), "um único dígito") errorBundlePretty(parse(p, "in", "x")) -- Saída: "in:1:1: unexpected input; expected: um único dígito"
Fornecido por familiaridade com o megaparsec; no BernParsec devolve o parser inalterado.
parseTest(try(string("ab")), "abc")
-- Saída: "ab"
Casa zero ou mais vezes, coletando uma lista. (Alias: zeroOrMore.)
parseTest(many(digitChar()), "123abc") -- Saída: ['1', '2', '3'] parseTest(many(digitChar()), "abc") -- Saída: []
Casa uma ou mais vezes. (Alias: oneOrMore.)
parseTest(some(letterChar()), "Bern2") -- Saída: ['B', 'e', 'r', 'n']
Casa exatamente n vezes. (Alias: repeatExactly.)
parseTest(count(4, digitChar()), "2026!") -- Saída: ['2', '0', '2', '6']
Casa no máximo uma vez, devolvendo um Maybe (BPJust valor ou BPNothing). (Alias: optionally.)
parseTest(optional(char('-')), "-5")
-- Saída: BPJust('-')
parseTest(optional(char('-')), "5")
-- Saída: BPNothing()
Zero ou mais itens separados por separador. (Alias: separatedBy.)
csv = sepBy(decimal(), char(','))
parseTest(csv, "1,2,3")
-- Saída: [1, 2, 3]
parseTest(csv, "")
-- Saída: []
Um ou mais itens separados por separador. (Alias: separatedBy1.)
parseTest(sepBy1(letterChar(), char('-')), "a-b-c")
-- Saída: ['a', 'b', 'c']
Repete parser até que parser_fim case; o terminador é consumido. (Alias: repeatUntil.)
comment = thenP(string("--"), manyTill(anySingle(), newline()))
parseTest(comment, "-- a note\nrest")
-- Saída: [' ', 'a', ' ', 'n', 'o', 't', 'e']
Casa parser entre abre e fecha, mantendo apenas o valor interno. (Alias: surroundedBy.)
quoted = between(char('"'), char('"'), many(noneOf("\"")))
parseTest(quoted, "\"hi\"")
-- Saída: ['h', 'i']
Casa parser mas não consome entrada - espia o que vem a seguir. (Alias: peek.)
parseTest(lookAhead(string("ab")), "abc")
-- Saída: "ab" (o cursor permanece na posição 0)
Tem sucesso apenas se parser não casar aqui; não consome nada. (Alias: notAhead.)
-- "let" não seguido imediatamente por uma letra (logo, não "letter")
kw = before(string("let"), notFollowedBy(letterChar()))
parseTest(kw, "let x")
-- Saída: "let"
Adia a construção de um parser até que seja necessário - a chave para gramáticas recursivas, em que um parser se refere a si mesmo. (Alias: deferred.)
def value() -> orElse(decimal(), between(char('('), char(')'), lazy(value)))
parseTest(value(), "(((7)))")
-- Saída: 7
Faz parse de um ou mais termos separados por op, dobrando-os de forma associativa à esquerda com combine(esquerda, valor_op, direita). Ideal para aritmética. (Alias: chainLeft.)
plus = mapP(char('+'), \_ -> "+")
sum = chainl1(decimal(), plus, \a, _, b -> a + b)
parseTest(sum, "1+2+3")
-- Saída: 6
Roda parser e então pula qualquer espaço em branco à direita - assim os tokens não precisam se preocupar com os espaços depois deles. (Alias: token.)
parseTest(lexeme(decimal()), "42 ") -- Saída: 42 (espaços à direita consumidos)
Casa o texto exato s como um token, pulando o espaço em branco à direita.
parseTest(sepBy1(symbol("ok"), symbol(",")), "ok , ok , ok")
-- Saída: ["ok", "ok", "ok"]
Um inteiro sem sinal (um ou mais dígitos). (Aliases: digits, wholeNumber.)
parseTest(decimal(), "123") -- Saída: 123
Um número com parte fracionária (dígitos, um ponto, dígitos). (Alias: decimalNumber.)
parseTest(float(), "12.50") -- Saída: 12.5
Permite um + ou - inicial opcional na frente de um parser de número, negando no -. (Alias: withSign.)
parseTest(signed(float()), "-3.5") -- Saída: -3.5
Um inteiro com sinal - signed(decimal()). (Alias: integerNumber.)
parseTest(integer(), "-42") -- Saída: -42
Invólucros que fazem parse de algo entre delimitadores e pulam o espaço em branco interno (construídos sobre symbol).
Faz parse de parser entre ( ), [ ] ou { }. (Aliases: inParens, inBrackets, inBraces.)
list = brackets(sepBy(lexeme(decimal()), symbol(",")))
parseTest(list, "[ 1, 2, 3 ]")
-- Saída: [1, 2, 3]
O BernParsec traz um conjunto de operadores infixos, inspirados no megaparsec e no Applicative do Haskell, para que uma gramática se leia quase como a coisa que ela descreve. São apenas os combinadores acima com símbolos, então tudo que você já sabe continua valendo.
| Operador | Igual a | Significado |
|---|---|---|
f <$> p | mapP(p, f) | aplica f ao que p parsear |
pf <*> px | applicative | sequencia dois parsers; aplica o resultado de pf ao de px |
p <* q | before(p, q) | roda os dois em ordem, mantém o resultado da esquerda |
p *> q | thenP(p, q) | roda os dois em ordem, mantém o resultado da direita |
p <|> q | orElse(p, q) | tenta p; se falhar, tenta q |
p >>= f | bind(p, f) | roda p e então constrói o próximo parser a partir do valor |
p <?> "lbl" | label(p, "lbl") | renomeia p para mensagens de erro melhores |
A precedência espelha o Haskell: <$> <*> <* *> prendem mais forte, depois <|>, depois >>=, depois <?> (o mais frouxo, então rotula um parser inteiro). Todos são associativos à esquerda.
import core
import vendor/bernparsec
-- um token: um número com sinal, espaços à direita consumidos
num = lexeme(signed(decimal()))
-- "uma etiqueta de preço como $12, mantém o número"
price = char('$') *> num
parseTest(price, "$12")
-- Saída: 12
-- "dois números separados por vírgula, entre colchetes"
pair = (\x, y -> [x, y]) <$> (num <* symbol(",")) <*> num
parseTest(inBrackets(pair), "[ 3, 4 ]")
-- Saída: [3, 4]
-- escolha + rótulo
keyword = string("if") <|> string("else") <|> string("end") > "a keyword"
parseTest(keyword, "else")
-- Saída: "else"
vendor/bernparsec com o recurso de operadores personalizados da Bern, e chegam ao seu arquivo pelo import como qualquer outra definição.
Todo combinador central tem um alias em inglês claro que encaminha para ele. Use o que ler melhor - ou misture os dois na mesma gramática.
| Alias amigável | Nome central |
|---|---|
literalChar | char |
anyChar | anySingle |
charIn | oneOf |
charNotIn | noneOf |
charWhere | satisfy |
text | string |
endOfInput | eof |
succeed | pure |
failWith | fail_parser |
digit / letter | digitChar / letterChar |
letterOrDigit | alphaNumChar |
whitespaceChar | spaceChar |
mapResult | mapP |
andThen | bind |
keepRight / keepLeft | thenP / before |
orTry | orElse |
firstOf | choice |
zeroOrMore / oneOrMore | many / some |
repeatExactly | count |
optionally | optional |
separatedBy / separatedBy1 | sepBy / sepBy1 |
repeatUntil | manyTill |
peek / notAhead | lookAhead / notFollowedBy |
describe | label |
deferred | lazy |
surroundedBy | between |
chainLeft | chainl1 |
token | lexeme |
digits / wholeNumber | decimal |
integerNumber | integer |
decimalNumber | float |
withSign | signed |
inParens / inBrackets / inBraces | parens / brackets / braces |
run / quickParse / prettyError | parse / parseTest / errorBundlePretty |
Todo parser também pode ser expresso como o ADT BPParser (os construtores BP*), o que é útil quando você quer um parser sobre o qual fazer pattern matching ou passar como dado. parser_from_adt / normalize_parser traduzem a forma ADT para a forma de dicionário que o interpretador roda.
-- um parser descrito como dado e então rodado
p = parser_from_adt(BPString("hi"))
parseTest(p, "hi there")
-- Saída: "hi"
Os valores de resultado, estado e Maybe também têm formas ADT - BPOk valor estado / BPErr expected message estado, BPState input source index line column e BPJust valor / BPNothing. As funções parseADT, runParserADT e parseTestADT rodam um parser e devolvem o resultado em ADT em vez do dicionário.
parseADT(string("hi"), "src", "hi")
-- Saída: BPOk("hi", BPState("hi", "src", 2, 1, 3))
Os combinadores brilham quando compostos em uma gramática real. Aqui está um pequeno avaliador aritmético que combina lexeme, parens, lazy, chainl1 e choice para parsear e avaliar expressões com a precedência correta:
import vendor/bernparsec
-- um número, ou uma expressão entre parênteses (recursão via lazy)
def factor() -> choice([
lexeme(signed(float())),
lexeme(signed(decimal())),
parens(lazy(expr))
])
-- '*' e '/' ligam mais forte que '+' e '-'
def mulOp() -> choice([
mapP(symbol("*"), \_ -> '*'),
mapP(symbol("/"), \_ -> '/')
])
def term() -> chainl1(factor(), mulOp(), \a, op, b ->
if op == '*' then a * b else a / b end)
def addOp() -> choice([
mapP(symbol("+"), \_ -> '+'),
mapP(symbol("-"), \_ -> '-')
])
def expr() -> chainl1(term(), addOp(), \a, op, b ->
if op == '+' then a + b else a - b end)
parseTest(expr(), "2 + 3 * 4")
-- Saída: 14
parseTest(expr(), "(2 + 3) * 4")
-- Saída: 20
E um parser de linha de configuração chave–valor que reúne some, between, sepBy e before:
import vendor/bernparsec
ident = mapP(some(letterChar()), \cs -> "" <> cs)
val = mapP(some(noneOf(";")), \cs -> "" <> cs)
pair = bind(before(ident, symbol("=")), \k ->
mapP(val, \v -> [k, v]))
config = sepBy(lexeme(pair), symbol(";"))
parseTest(config, "host=localhost; port=8080")
-- Saída: [["host", "localhost"], ["port", "8080"]]