DEV Community

Cover image for Como criar uma linguagem usando ANTLR4 e Java
Victor Osório
Victor Osório

Posted on • Updated on

Como criar uma linguagem usando ANTLR4 e Java

Ao desenvolver qualquer ferramenta, você topará com a ingrata tarefa de escrever um parser. Usei o termo ingrata porque a principio pode parecer fácil, mas depois você verá que é bem complicado.

O problema na maioria das vezes não é o parser, mas a infinidade de possibilidades que surgem ao se propor uma solução tão aberta a ponto de precisar de uma linguagem.

Antes do Parser

Para se precisar de um parser é primeiro necessário um problema que se precisa de uma linguagem. Temos dois tipos de linguagem em computação: Linguagem Formal e Linguagem Natural.

Parsers resolvem o problema de se compreender uma Linguagem Formal, mas precisamos entender o que é cada uma.

Linguagem Natural

Vamos primeiro iniciar pela que aprendemos primeiro. Linguagem Natural é a que estamos tentando ensinar para o meu filho de 1 ano e meio 👶. Devido a uma exposição a telas, ele acabou desenvolvendo outras linguagens antes da qual será a principal em sua vida. Ele já compreende o português, compreende algumas músicas, consegue cantarolar.... Mas não consegue falar ainda. Só o papai e mamãe, mas ainda não associa a fala dele as coisas. Ele associa o som nosso as coisas. Bizarro não?

Mas essa é a forma de compreensão nossa. Temos sons, falas, fonemas, etc... Eles existem apesar da gramática. A gramática serve como base, serve para criar uma linguagem comum onde todos podemos ser compreendidos.

Em uma linguagem natural, ela já existe antes de uma gramática. A gramática vem para normalizar ela.

Temos inúmeras Linguagens Natural no mundo:

  • Música ➡️ Partituras
  • Fala ➡️ Português, Inglês, Espanhol, etc....
  • UX ➡️ Aplicativo Mobile, Aplicativo Web, etc...

Há inúmeras formas de Linguagem Natural, se formos analisar filosoficamente, uma Linguagem Natural é qualquer formas de símbolos gerados e consumidor conscientemente por humanos.

Quer saber como processar uma Linguagem Natural. Procure por RNN ou LSTM.

Linguagem Formal

Apesar de usar a mesma palavra, uma Linguagem Formal ela é completamente diferente de uma Linguagem Natural. Uma Linguagem Formal tem um fim especifico, seja ele dar ordens a uma máquina ou a troca de informações entre sistemas.

Agora não sei se você percebeu a principal diferença: Linguagens Formais são feitas para serem compreendidas por Máquinas.

Você pode ter a fé que for em Tecnologia, mas a grande diferença é que nunca existirá um Guimarães Rosa da computação, pois Linguagens Formais não aceitam neologismos. Ou a linguagem segue estritamente ao binômio gramática e sintaxe, ou ela não é compreendida. Um compilador não entende aquilo que é fora do que já conhece.

Tá, e quais são as Linguagens Formais conhecidas:

  • C
  • C++
  • Java
  • XML
  • brainfuck!!! Um dia aprenderei brainfuck!!!

Segue o Hello World em brainfuck:

+[-[<<[+[--->]-[<<<]]]>>>-]>-.---.>..>.<<<<-.<+.>>>>>.>.<<.<-.
Enter fullscreen mode Exit fullscreen mode

A Necessidade

Tá, mais porque vou precisar de uma nova linguagem? Bom, antes disso é preciso de uma necessidade. Eu acabei desenvolvendo uma em casa nos últimos dias como um exercício. A necessidade era: em uma reunião, todos odeia o JMeter.

A razão de todos odiarem o JMeter era óbvia, o JMeter usa o XML como Linguagem e XML é apenas uma Linguagem para armazenar informações, ela não é desenvolvida para se processar testes.

Então fiz esse exercício: Como seria uma Linguagem de Testes?

Observação: Esse é um exercício e existem outras linguagens para Testes. Mas poucas substituem o que o JMeter faz...

Criando o Parser

A partir desse ponto, irei me referir a essa linguagem que estou desenvolvendo como PlainTest.

Imaginar

O primeiro passo para se projetar uma Linguagem Formal é imaginar uma gramática básica.

Para a PlainTest, eu imaginei duas unidades básicas:

  • A Suite: É um agrupamento ordenado de passos ou suites. Serve como agrupamento lógico. Seria o CriarUsuário.
  • O Step: É a únidade básica do Teste, ou seja a realização dele. Executar um comando, enviar uma Request HTTP, etc...

Segue o meu primeiro exemplo de PlainTest:

Suite UserTest {
    HTTP CreateUser {
        url   : "http://127.0.0.1"
        method: "POST"
        body  : """
                   {
                       "id": 123,
                       "firstName": "John",
                       "lastName" : "Doe"
                   }
                """
        assert responseCode Equals 200
    }
}
Enter fullscreen mode Exit fullscreen mode

Extrair

Imaginada a gramática, é preciso extrair dela algumas informações: seu léxico e sua gramática. Para isso precisaremos de uma Linguagem Formal, e por isso escolheremos o ANTLR4. O ANTLR4 tem uma gramática própria onde o desenvolvedor pode declarar a gramática e o léxico da sua linguagem.

Léxico

Vamos definir grosseiramente o Léxico, ou Tokenização, como a identificação de cada elemento da Linguagem.

Assim podemos definir na nossa linguagem:

  • Reserved Words: Suite, assert
  • Identifier: Serve para identificar elementos
  • Número: sim, um número....
  • String: sim, uma string...
  • MultilineString: String definida por """ e que não necessita escapes.

Para definir um Token em ANTLR4, é preciso definir um identificar com letra maiúscula:

grammar TestSuite;

STRING : DQUOTE (ESC | ~["\\])* DQUOTE;

VERB: 'Contains' | 'Equals';

IDENTIFIER: [A-Za-z] [._\-A-Za-z0-9]*;

NUMBER: '-'? INT '.' [0-9]+ EXP? | '-'? INT EXP | '-'? INT;

fragment DQUOTE: '"';

fragment ESC: '\\' (["\\/bfnrt] | UNICODE);

fragment UNICODE: 'u' HEX HEX HEX HEX;

fragment HEX: [0-9a-fA-F];

fragment INT: '0' | [1-9] [0-9]*; // no leading zeros

fragment EXP: [Ee] [+\-]? INT; // \- since - means "range" inside [...]

// Just ignore WhiteSpaces
WS: [ \t\r\n]+ -> skip;
Enter fullscreen mode Exit fullscreen mode

Na gramática acima definimos apenas alguns tokens. Observe que existem Tokens e fragmentos. No ANTLR4, o fragment deve ser usado porque um identificador não aceita ser composto por identificadores, ele deve ser composto por algo similar a um Regex e fragmentos.

Sintático

Quando me refiro a Sintático em ANTLR4, estou falando da gramática em si, as regras. No nosso caso iremos criar os seguintes valores:

  • Suite: Composto por Suites e Steps
  • Step: Unidade básica do test
  • Attribute: Um par de Chave/Valor
  • Assertion: Um par de Chave/Valor
  • Value: Um valor que pode ser de Attribute ou Assertion.

Cada regra dessa será uma Parser Rule na gramática do ANTLR4. Na definição da gramática elas são diferenciadas pela primeira letra. O Léxico é maiúscula, enquanto o Sintático é minúsculo. Posteriormente veremos a diferença na prática.

Segue o exemplo de como ficaria nossa Suite definida:

grammar TestSuite;

suite:
    'Suite' IDENTIFIER '{'
        (suite | step)* 
    '}'
;

step:
    IDENTIFIER IDENTIFIER '{'
        (assertion | attribute)* 
    '}'
;

assertion: 'assert' IDENTIFIER VERB value;

attribute: IDENTIFIER ':' value;

value: NUMBER | STRING;

STRING : DQUOTE (ESC | ~["\\])* DQUOTE;

VERB: 'Contains' | 'Equals';

IDENTIFIER: [A-Za-z] [._\-A-Za-z0-9]*;

NUMBER: '-'? INT '.' [0-9]+ EXP? | '-'? INT EXP | '-'? INT;

fragment DQUOTE: '"';

fragment ESC: '\\' (["\\/bfnrt] | UNICODE);

fragment UNICODE: 'u' HEX HEX HEX HEX;

fragment HEX: [0-9a-fA-F];

fragment INT: '0' | [1-9] [0-9]*; // no leading zeros

fragment EXP: [Ee] [+\-]? INT; // \- since - means "range" inside [...]

// Just ignore WhiteSpaces
WS: [ \t\r\n]+ -> skip;
Enter fullscreen mode Exit fullscreen mode

Gerando o Código

Essa Gramática não ir se plugar automaticamente no código, antes disso é necessário gerar alguns códigos antes.

Nesse ponto recomendo fortemente usar o Maven para gerenciar o ANTLR4, assim você já terá tudo configurado facilmente.

Mas caso queira gerar manualmente...

Gerando Código Manualmente

  1. Faça o download do ANTLR4 Tool em Download ANTLR, procura por ANTLR tool itself
  2. Depois execute o ANTLR4 Tool:

    java -jar ~/Downloads/antlr-4.8-complete.jar -package io.vepo.tutorial.antlr4.generated src/main/antlr4/io/vepo/tutorial/antlr4/generated/TestSuite.g4
    
  3. Use os arquivos gerados no seu projeto, conforme abaixo:
    Arquivos ANTLR4

Gerando usando o Maven

Usando o Maven é bem mais simples. Crie seu arquivo .g4 no diretório similar ao pacote. Por exemplo, em nosso exemplo está:

ANTLR4 Gramática

E depois configure o plugin do ANTLR4 em seu pom.xml

<plugin>
    <groupId>org.antlr</groupId>
    <artifactId>antlr4-maven-plugin</artifactId>
    <version>${version.antlr4}</version>
    <configuration>
        <arguments>
            <argument>-package</argument>
            <argument>io.vepo.tutorial.antlr4.generated</argument>
        </arguments>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>antlr4</goal>
            </goals>
        </execution>
    </executions>
</plugin>
Enter fullscreen mode Exit fullscreen mode

Usando o Código Gerado

Do código gerado, será preciso apenas reimplementar uma classe. Veja a interface TestSuiteListener, para cada Regra é chamado um Método antes e depois de processado. Em cada método há um objeto de contexto onde podemos acessar todos os Tokens.

Esses métodos são chamados em ordem, assim para o exemplo abaixo, qualquer metodo do atributo url será chamado antes de method e assim por diante. Todos como tem apenas uma suite, o enterSuite e o exitSuite serão o primeiro e último a serem chamados.

Suite UserTest {
    HTTP CreateUser {
        url   : "http://127.0.0.1"
        method: "POST"
        body  : """
                   {
                       "id": 123,
                       "firstName": "John",
                       "lastName" : "Doe"
                   }
                """
        assert responseCode Equals 200
    }
}
Enter fullscreen mode Exit fullscreen mode

Por fim, para transformar texto em Objetos, basta chamar o parser?

TestSuiteParser parser = new TestSuiteParser(
        new CommonTokenStream(new TestSuiteLexer(CharStreams.fromString(contents))));
ParseTreeWalker walker = new ParseTreeWalker();
SuiteCreator creator = new SuiteCreator();
walker.walk(creator, parser.suite());
Suite suite = creator.getTestSuite();
Enter fullscreen mode Exit fullscreen mode

Conclusão

Existem problemas que só podem ser resolvidos criando uma linguagem. Se usarmos padrões como JSON ou XML vamos complicar mais que simplificar.

Para essas soluções, é mais fácil usar o ANTLR. Assim criamos uma linguagem facilmente.

Recursos

Todo o código usado nesse post está disponível no repositório:

ATNRL4 Parser

To see it in action:

mvn clean verify
Enter fullscreen mode Exit fullscreen mode





Continuarei o desenvolvimento dessa tool em:

GitHub logo vepo / plain-test

Just a tool for testing. No XML! Just a plain text language...

Plain Test

Rational

Testing is hard. If there is an application that executes tests from plain test files?

Format

Suite UserTest {
    HTTP CreateUser {
        url   : "http://127.0.0.1"
        method: "POST"
        body  : """
                   {
                       "id": 123,
                       "firstName": "John",
                       "lastName" : "Doe"
                   }
                """
        assert responseCode: 200
    }
}



Qualquer ajuda é bem vinda....

Top comments (1)

Collapse
 
isacdutch profile image
Mr. Isaque Oliveira - יצחק

Muito bom documento. O texto bem desenvolvido. Congratulations!!!😂