Como criar uma linguagem usando ANTLR4 e Java

Como criar uma linguagem usando ANTLR4 e Java

Post originalmente publicado em dev.to.

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:

+[-[<<[+[--->]-[<<<]]]>>>-]>-.---.>..>.<<<<-.<+.>>>>>.>.<<.<-.

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

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;

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;

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>

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

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

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 github.com/vepo/antlr4-post

Continuarei o desenvolvimento dessa tool em Plain Test github.com/vepo/plain-test. Qualquer ajuda é bem vinda….

Licença Creative Commons
Este obra está licenciado com uma Licença Creative Commons Atribuição-NãoComercial-CompartilhaIgual 4.0 Internacional .