vendor/bernparsec Vendor

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

A ideia central

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.

Rodando um parser

Rodar um parser contra a entrada produz um objeto de resultado:

CampoSignificado
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)
parse(parser, nome_fonte, entrada) → result

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"
parseTest(parser, entrada) → valor | string

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'"
errorBundlePretty(result) → string

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'"

Parsers primitivos

char(c) → parser

Casa exatamente o caractere c.

parseTest(char('a'), "abc")
-- Saída: 'a'
string(token) → parser

Casa o texto exato token.

parseTest(string("let"), "let x = 1")
-- Saída: "let"
anySingle() → parser

Casa qualquer caractere único.

parseTest(anySingle(), "Z!")
-- Saída: 'Z'
oneOf(chars) → parser

Casa qualquer caractere que apareça em chars.

parseTest(oneOf("+-*/"), "*3")
-- Saída: '*'
noneOf(chars) → parser

Casa qualquer caractere que não apareça em chars.

parseTest(noneOf(" \t\n"), "hi")
-- Saída: 'h'
satisfy(pred) · satisfy(pred, label) → parser

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'
eof() → parser

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)
pure(valor) → parser

Sempre tem sucesso, produzindo valor sem consumir entrada. (Alias: succeed.)

parseTest(pure(42), "qualquer coisa")
-- Saída: 42
fail_parser(mensagem) → parser

Sempre falha com mensagem. (Alias: failWith.)

parseTest(fail_parser("nope"), "abc")
-- Saída: ":1:1: nope; expected: "

Classes de caractere

Parsers satisfy prontos para classes comuns, cada um com um rótulo de erro sensato.

digitChar() → parser

Um dígito 09.

parseTest(digitChar(), "7x")
-- Saída: '7'
letterChar() → parser

Uma letra az / AZ.

parseTest(letterChar(), "Bern")
-- Saída: 'B'
alphaNumChar() → parser

Uma letra ou dígito.

parseTest(some(alphaNumChar()), "id42 ")
-- Saída: ['i', 'd', '4', '2']
spaceChar() · newline() · tab() → parser

Um caractere de espaço em branco, uma quebra de linha ou um tab, respectivamente.

parseTest(spaceChar(), " x")
-- Saída: ' '
space() · space1() → parser

space() casa zero ou mais espaços em branco; space1() exige pelo menos um.

parseTest(space(), "   hi")
-- Saída: [' ', ' ', ' ']

Sequência & mapeamento

mapP(parser, mapper) → parser

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)
thenP(esquerda, direita) → parser

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)
before(esquerda, direita) → parser

Roda os dois em ordem, mantém o valor da esquerda. (Alias: keepLeft.)

parseTest(before(decimal(), char('%')), "75%")
-- Saída: 75
bind(parser, to_parser) → parser

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']

Escolha & falha

orElse(esquerda, direita) → parser

Tenta esquerda; se falhar, tenta direita. (Alias: orTry.)

yesNo = orElse(string("yes"), string("no"))
parseTest(yesNo, "no")
-- Saída: "no"
choice(parsers) → parser

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"
label(parser, rótulo_esperado) → parser

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"
try(parser) → parser

Fornecido por familiaridade com o megaparsec; no BernParsec devolve o parser inalterado.

parseTest(try(string("ab")), "abc")
-- Saída: "ab"

Repetição

many(parser) → parser

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: []
some(parser) → parser

Casa uma ou mais vezes. (Alias: oneOrMore.)

parseTest(some(letterChar()), "Bern2")
-- Saída: ['B', 'e', 'r', 'n']
count(n, parser) → parser

Casa exatamente n vezes. (Alias: repeatExactly.)

parseTest(count(4, digitChar()), "2026!")
-- Saída: ['2', '0', '2', '6']
optional(parser) → parser

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()
sepBy(parser, separador) → parser

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: []
sepBy1(parser, separador) → parser

Um ou mais itens separados por separador. (Alias: separatedBy1.)

parseTest(sepBy1(letterChar(), char('-')), "a-b-c")
-- Saída: ['a', 'b', 'c']
manyTill(parser, parser_fim) → parser

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']
between(abre, fecha, parser) → parser

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']

Look-around

lookAhead(parser) → parser

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)
notFollowedBy(parser) → parser

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"

Recursão & operadores

lazy(thunk) → parser

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
chainl1(termo, op, combine) → parser

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

Lexing & espaços

lexeme(parser) → parser

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)
symbol(s) → parser

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"]

Números

decimal() → parser

Um inteiro sem sinal (um ou mais dígitos). (Aliases: digits, wholeNumber.)

parseTest(decimal(), "123")
-- Saída: 123
float() → parser

Um número com parte fracionária (dígitos, um ponto, dígitos). (Alias: decimalNumber.)

parseTest(float(), "12.50")
-- Saída: 12.5
signed(parser_número) → parser

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
integer() → parser

Um inteiro com sinal - signed(decimal()). (Alias: integerNumber.)

parseTest(integer(), "-42")
-- Saída: -42

Delimitadores

Invólucros que fazem parse de algo entre delimitadores e pulam o espaço em branco interno (construídos sobre symbol).

parens(parser) · brackets(parser) · braces(parser) → parser

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]

Operadores Bern 2.1

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.

OperadorIgual aSignificado
f <$> pmapP(p, f)aplica f ao que p parsear
pf <*> pxapplicativesequencia dois parsers; aplica o resultado de pf ao de px
p <* qbefore(p, q)roda os dois em ordem, mantém o resultado da esquerda
p *> qthenP(p, q)roda os dois em ordem, mantém o resultado da direita
p <|> qorElse(p, q)tenta p; se falhar, tenta q
p >>= fbind(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.

Uma gramática que se lê como o seu formato

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"
Nota: esses operadores são definidos dentro de vendor/bernparsec com o recurso de operadores personalizados da Bern, e chegam ao seu arquivo pelo import como qualquer outra definição.

Aliases amigáveis

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ávelNome central
literalCharchar
anyCharanySingle
charInoneOf
charNotInnoneOf
charWheresatisfy
textstring
endOfInputeof
succeedpure
failWithfail_parser
digit / letterdigitChar / letterChar
letterOrDigitalphaNumChar
whitespaceCharspaceChar
mapResultmapP
andThenbind
keepRight / keepLeftthenP / before
orTryorElse
firstOfchoice
zeroOrMore / oneOrMoremany / some
repeatExactlycount
optionallyoptional
separatedBy / separatedBy1sepBy / sepBy1
repeatUntilmanyTill
peek / notAheadlookAhead / notFollowedBy
describelabel
deferredlazy
surroundedBybetween
chainLeftchainl1
tokenlexeme
digits / wholeNumberdecimal
integerNumberinteger
decimalNumberfloat
withSignsigned
inParens / inBrackets / inBracesparens / brackets / braces
run / quickParse / prettyErrorparse / parseTest / errorBundlePretty

Avançado: forma ADT

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))

Juntando tudo

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:

Uma calculadora de quatro operações

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:

Parseando pares "chave=valor"

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"]]